mirror of
https://github.com/IBM/fp-go.git
synced 2026-01-17 00:53:55 +02:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fd5b90138 | ||
|
|
cdc2041d8e | ||
|
|
777fff9a5a | ||
|
|
8acea9043f | ||
|
|
c6445ac021 |
1
v2/.bobignore
Normal file
1
v2/.bobignore
Normal file
@@ -0,0 +1 @@
|
||||
reflect\reflect.go
|
||||
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
|
||||
|
||||
@@ -514,6 +514,83 @@ func Push[A any](a A) Operator[A, A] {
|
||||
return G.Push[Operator[A, A]](a)
|
||||
}
|
||||
|
||||
// Concat concatenates two arrays, appending the provided array to the end of the input array.
|
||||
// This is a curried function that takes an array to append and returns a function that
|
||||
// takes the base array and returns the concatenated result.
|
||||
//
|
||||
// The function creates a new array containing all elements from the base array followed
|
||||
// by all elements from the appended array. Neither input array is modified.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of elements in the arrays
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The array to append to the end of the base array
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a base array and returns a new array with `as` appended to its end
|
||||
//
|
||||
// Behavior:
|
||||
// - Creates a new array with length equal to the sum of both input arrays
|
||||
// - Copies all elements from the base array first
|
||||
// - Appends all elements from the `as` array at the end
|
||||
// - Returns the base array unchanged if `as` is empty
|
||||
// - Returns `as` unchanged if the base array is empty
|
||||
// - Does not modify either input array
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// base := []int{1, 2, 3}
|
||||
// toAppend := []int{4, 5, 6}
|
||||
// result := array.Concat(toAppend)(base)
|
||||
// // result: []int{1, 2, 3, 4, 5, 6}
|
||||
// // base: []int{1, 2, 3} (unchanged)
|
||||
// // toAppend: []int{4, 5, 6} (unchanged)
|
||||
//
|
||||
// Example with empty arrays:
|
||||
//
|
||||
// base := []int{1, 2, 3}
|
||||
// empty := []int{}
|
||||
// result := array.Concat(empty)(base)
|
||||
// // result: []int{1, 2, 3}
|
||||
//
|
||||
// Example with strings:
|
||||
//
|
||||
// words1 := []string{"hello", "world"}
|
||||
// words2 := []string{"foo", "bar"}
|
||||
// result := array.Concat(words2)(words1)
|
||||
// // result: []string{"hello", "world", "foo", "bar"}
|
||||
//
|
||||
// Example with functional composition:
|
||||
//
|
||||
// numbers := []int{1, 2, 3}
|
||||
// result := F.Pipe2(
|
||||
// numbers,
|
||||
// array.Map(N.Mul(2)),
|
||||
// array.Concat([]int{10, 20}),
|
||||
// )
|
||||
// // result: []int{2, 4, 6, 10, 20}
|
||||
//
|
||||
// Use cases:
|
||||
// - Combining multiple arrays into one
|
||||
// - Building arrays incrementally
|
||||
// - Implementing array-based data structures (queues, buffers)
|
||||
// - Merging results from multiple operations
|
||||
// - Creating array pipelines with functional composition
|
||||
//
|
||||
// Performance:
|
||||
// - Time complexity: O(n + m) where n and m are the lengths of the arrays
|
||||
// - Space complexity: O(n + m) for the new array
|
||||
// - Optimized to avoid allocation when one array is empty
|
||||
//
|
||||
// Note: This function is immutable - it creates a new array rather than modifying
|
||||
// the input arrays. For appending a single element, consider using Append or Push.
|
||||
//
|
||||
//go:inline
|
||||
func Concat[A any](as []A) Operator[A, A] {
|
||||
return F.Bind2nd(array.Concat[[]A, A], as)
|
||||
}
|
||||
|
||||
// MonadFlap applies a value to an array of functions, producing an array of results.
|
||||
// This is the monadic version that takes both parameters.
|
||||
//
|
||||
|
||||
@@ -764,3 +764,341 @@ func TestExtendUseCases(t *testing.T) {
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestConcat tests the Concat function
|
||||
func TestConcat(t *testing.T) {
|
||||
t.Run("Concat two non-empty arrays", func(t *testing.T) {
|
||||
base := []int{1, 2, 3}
|
||||
toAppend := []int{4, 5, 6}
|
||||
result := Concat(toAppend)(base)
|
||||
expected := []int{1, 2, 3, 4, 5, 6}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Concat with empty array to append", func(t *testing.T) {
|
||||
base := []int{1, 2, 3}
|
||||
empty := []int{}
|
||||
result := Concat(empty)(base)
|
||||
assert.Equal(t, base, result)
|
||||
})
|
||||
|
||||
t.Run("Concat to empty base array", func(t *testing.T) {
|
||||
empty := []int{}
|
||||
toAppend := []int{1, 2, 3}
|
||||
result := Concat(toAppend)(empty)
|
||||
assert.Equal(t, toAppend, result)
|
||||
})
|
||||
|
||||
t.Run("Concat two empty arrays", func(t *testing.T) {
|
||||
empty1 := []int{}
|
||||
empty2 := []int{}
|
||||
result := Concat(empty2)(empty1)
|
||||
assert.Equal(t, []int{}, result)
|
||||
})
|
||||
|
||||
t.Run("Concat strings", func(t *testing.T) {
|
||||
words1 := []string{"hello", "world"}
|
||||
words2 := []string{"foo", "bar"}
|
||||
result := Concat(words2)(words1)
|
||||
expected := []string{"hello", "world", "foo", "bar"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Concat single element arrays", func(t *testing.T) {
|
||||
arr1 := []int{1}
|
||||
arr2 := []int{2}
|
||||
result := Concat(arr2)(arr1)
|
||||
expected := []int{1, 2}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Does not modify original arrays", func(t *testing.T) {
|
||||
base := []int{1, 2, 3}
|
||||
toAppend := []int{4, 5, 6}
|
||||
baseCopy := []int{1, 2, 3}
|
||||
toAppendCopy := []int{4, 5, 6}
|
||||
|
||||
_ = Concat(toAppend)(base)
|
||||
|
||||
assert.Equal(t, baseCopy, base)
|
||||
assert.Equal(t, toAppendCopy, toAppend)
|
||||
})
|
||||
|
||||
t.Run("Concat with floats", func(t *testing.T) {
|
||||
arr1 := []float64{1.1, 2.2}
|
||||
arr2 := []float64{3.3, 4.4}
|
||||
result := Concat(arr2)(arr1)
|
||||
expected := []float64{1.1, 2.2, 3.3, 4.4}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Concat with structs", func(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
arr1 := []Person{{"Alice", 30}, {"Bob", 25}}
|
||||
arr2 := []Person{{"Charlie", 35}}
|
||||
result := Concat(arr2)(arr1)
|
||||
expected := []Person{{"Alice", 30}, {"Bob", 25}, {"Charlie", 35}}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Concat large arrays", func(t *testing.T) {
|
||||
arr1 := MakeBy(500, F.Identity[int])
|
||||
arr2 := MakeBy(500, func(i int) int { return i + 500 })
|
||||
result := Concat(arr2)(arr1)
|
||||
|
||||
assert.Equal(t, 1000, len(result))
|
||||
assert.Equal(t, 0, result[0])
|
||||
assert.Equal(t, 499, result[499])
|
||||
assert.Equal(t, 500, result[500])
|
||||
assert.Equal(t, 999, result[999])
|
||||
})
|
||||
|
||||
t.Run("Concat multiple times", func(t *testing.T) {
|
||||
arr1 := []int{1}
|
||||
arr2 := []int{2}
|
||||
arr3 := []int{3}
|
||||
|
||||
result := F.Pipe2(
|
||||
arr1,
|
||||
Concat(arr2),
|
||||
Concat(arr3),
|
||||
)
|
||||
|
||||
expected := []int{1, 2, 3}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestConcatComposition tests Concat with other array operations
|
||||
func TestConcatComposition(t *testing.T) {
|
||||
t.Run("Concat after Map", func(t *testing.T) {
|
||||
numbers := []int{1, 2, 3}
|
||||
result := F.Pipe2(
|
||||
numbers,
|
||||
Map(N.Mul(2)),
|
||||
Concat([]int{10, 20}),
|
||||
)
|
||||
expected := []int{2, 4, 6, 10, 20}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Map after Concat", func(t *testing.T) {
|
||||
arr1 := []int{1, 2}
|
||||
arr2 := []int{3, 4}
|
||||
result := F.Pipe2(
|
||||
arr1,
|
||||
Concat(arr2),
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
expected := []int{2, 4, 6, 8}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Concat with Filter", func(t *testing.T) {
|
||||
arr1 := []int{1, 2, 3, 4}
|
||||
arr2 := []int{5, 6, 7, 8}
|
||||
result := F.Pipe2(
|
||||
arr1,
|
||||
Concat(arr2),
|
||||
Filter(func(n int) bool { return n%2 == 0 }),
|
||||
)
|
||||
expected := []int{2, 4, 6, 8}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Concat with Reduce", func(t *testing.T) {
|
||||
arr1 := []int{1, 2, 3}
|
||||
arr2 := []int{4, 5, 6}
|
||||
result := F.Pipe2(
|
||||
arr1,
|
||||
Concat(arr2),
|
||||
Reduce(func(acc, x int) int { return acc + x }, 0),
|
||||
)
|
||||
expected := 21 // 1+2+3+4+5+6
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Concat with Reverse", func(t *testing.T) {
|
||||
arr1 := []int{1, 2, 3}
|
||||
arr2 := []int{4, 5, 6}
|
||||
result := F.Pipe2(
|
||||
arr1,
|
||||
Concat(arr2),
|
||||
Reverse[int],
|
||||
)
|
||||
expected := []int{6, 5, 4, 3, 2, 1}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Concat with Flatten", func(t *testing.T) {
|
||||
arr1 := [][]int{{1, 2}, {3, 4}}
|
||||
arr2 := [][]int{{5, 6}}
|
||||
result := F.Pipe2(
|
||||
arr1,
|
||||
Concat(arr2),
|
||||
Flatten[int],
|
||||
)
|
||||
expected := []int{1, 2, 3, 4, 5, 6}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Multiple Concat operations", func(t *testing.T) {
|
||||
arr1 := []int{1}
|
||||
arr2 := []int{2}
|
||||
arr3 := []int{3}
|
||||
arr4 := []int{4}
|
||||
|
||||
result := Concat(arr4)(Concat(arr3)(Concat(arr2)(arr1)))
|
||||
|
||||
expected := []int{1, 2, 3, 4}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestConcatUseCases demonstrates practical use cases for Concat
|
||||
func TestConcatUseCases(t *testing.T) {
|
||||
t.Run("Building array incrementally", func(t *testing.T) {
|
||||
header := []string{"Name", "Age"}
|
||||
data := []string{"Alice", "30"}
|
||||
footer := []string{"Total: 1"}
|
||||
|
||||
result := F.Pipe2(
|
||||
header,
|
||||
Concat(data),
|
||||
Concat(footer),
|
||||
)
|
||||
|
||||
expected := []string{"Name", "Age", "Alice", "30", "Total: 1"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Merging results from multiple operations", func(t *testing.T) {
|
||||
evens := Filter(func(n int) bool { return n%2 == 0 })([]int{1, 2, 3, 4, 5, 6})
|
||||
odds := Filter(func(n int) bool { return n%2 != 0 })([]int{1, 2, 3, 4, 5, 6})
|
||||
|
||||
result := Concat(odds)(evens)
|
||||
expected := []int{2, 4, 6, 1, 3, 5}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Combining prefix and suffix", func(t *testing.T) {
|
||||
prefix := []string{"Mr.", "Dr."}
|
||||
names := []string{"Smith", "Jones"}
|
||||
|
||||
addPrefix := func(name string) []string {
|
||||
return Map(func(p string) string { return p + " " + name })(prefix)
|
||||
}
|
||||
|
||||
result := F.Pipe2(
|
||||
names,
|
||||
Chain(addPrefix),
|
||||
F.Identity[[]string],
|
||||
)
|
||||
|
||||
expected := []string{"Mr. Smith", "Dr. Smith", "Mr. Jones", "Dr. Jones"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Queue-like behavior", func(t *testing.T) {
|
||||
queue := []int{1, 2, 3}
|
||||
newItems := []int{4, 5}
|
||||
|
||||
// Add items to end of queue
|
||||
updatedQueue := Concat(newItems)(queue)
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, updatedQueue)
|
||||
assert.Equal(t, 1, updatedQueue[0]) // Front of queue
|
||||
assert.Equal(t, 5, updatedQueue[len(updatedQueue)-1]) // Back of queue
|
||||
})
|
||||
|
||||
t.Run("Combining configuration arrays", func(t *testing.T) {
|
||||
defaultConfig := []string{"--verbose", "--color"}
|
||||
userConfig := []string{"--output=file.txt", "--format=json"}
|
||||
|
||||
finalConfig := Concat(userConfig)(defaultConfig)
|
||||
|
||||
expected := []string{"--verbose", "--color", "--output=file.txt", "--format=json"}
|
||||
assert.Equal(t, expected, finalConfig)
|
||||
})
|
||||
}
|
||||
|
||||
// TestConcatProperties tests mathematical properties of Concat
|
||||
func TestConcatProperties(t *testing.T) {
|
||||
t.Run("Associativity: (a + b) + c == a + (b + c)", func(t *testing.T) {
|
||||
a := []int{1, 2}
|
||||
b := []int{3, 4}
|
||||
c := []int{5, 6}
|
||||
|
||||
// (a + b) + c
|
||||
left := Concat(c)(Concat(b)(a))
|
||||
|
||||
// a + (b + c)
|
||||
right := Concat(Concat(c)(b))(a)
|
||||
|
||||
assert.Equal(t, left, right)
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5, 6}, left)
|
||||
})
|
||||
|
||||
t.Run("Identity: a + [] == a and [] + a == a", func(t *testing.T) {
|
||||
arr := []int{1, 2, 3}
|
||||
empty := []int{}
|
||||
|
||||
// Right identity
|
||||
rightResult := Concat(empty)(arr)
|
||||
assert.Equal(t, arr, rightResult)
|
||||
|
||||
// Left identity
|
||||
leftResult := Concat(arr)(empty)
|
||||
assert.Equal(t, arr, leftResult)
|
||||
})
|
||||
|
||||
t.Run("Length property: len(a + b) == len(a) + len(b)", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
arr1 []int
|
||||
arr2 []int
|
||||
}{
|
||||
{[]int{1, 2, 3}, []int{4, 5}},
|
||||
{[]int{1}, []int{2, 3, 4, 5}},
|
||||
{[]int{}, []int{1, 2, 3}},
|
||||
{[]int{1, 2, 3}, []int{}},
|
||||
{MakeBy(100, F.Identity[int]), MakeBy(50, F.Identity[int])},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
result := Concat(tc.arr2)(tc.arr1)
|
||||
expectedLen := len(tc.arr1) + len(tc.arr2)
|
||||
assert.Equal(t, expectedLen, len(result))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Order preservation: elements maintain their relative order", func(t *testing.T) {
|
||||
arr1 := []int{1, 2, 3}
|
||||
arr2 := []int{4, 5, 6}
|
||||
result := Concat(arr2)(arr1)
|
||||
|
||||
// Check arr1 elements are in order
|
||||
assert.Equal(t, 1, result[0])
|
||||
assert.Equal(t, 2, result[1])
|
||||
assert.Equal(t, 3, result[2])
|
||||
|
||||
// Check arr2 elements are in order after arr1
|
||||
assert.Equal(t, 4, result[3])
|
||||
assert.Equal(t, 5, result[4])
|
||||
assert.Equal(t, 6, result[5])
|
||||
})
|
||||
|
||||
t.Run("Immutability: original arrays are not modified", func(t *testing.T) {
|
||||
original1 := []int{1, 2, 3}
|
||||
original2 := []int{4, 5, 6}
|
||||
copy1 := []int{1, 2, 3}
|
||||
copy2 := []int{4, 5, 6}
|
||||
|
||||
_ = Concat(original2)(original1)
|
||||
|
||||
assert.Equal(t, copy1, original1)
|
||||
assert.Equal(t, copy2, original2)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,81 @@
|
||||
// Copyright (c) 2024 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package builder provides a generic Builder pattern interface for constructing
|
||||
// complex objects with validation.
|
||||
//
|
||||
// The Builder pattern is useful when:
|
||||
// - Object construction requires multiple steps
|
||||
// - Construction may fail with validation errors
|
||||
// - You want to separate construction logic from the object itself
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// type PersonBuilder struct {
|
||||
// name string
|
||||
// age int
|
||||
// }
|
||||
//
|
||||
// func (b PersonBuilder) Build() result.Result[Person] {
|
||||
// if b.name == "" {
|
||||
// return result.Error[Person](errors.New("name is required"))
|
||||
// }
|
||||
// if b.age < 0 {
|
||||
// return result.Error[Person](errors.New("age must be non-negative"))
|
||||
// }
|
||||
// return result.Of(Person{Name: b.name, Age: b.age})
|
||||
// }
|
||||
package builder
|
||||
|
||||
type (
|
||||
// Builder is a generic interface for the Builder pattern that constructs
|
||||
// objects of type T with validation.
|
||||
//
|
||||
// The Build method returns a Result[T] which can be either:
|
||||
// - Success: containing the constructed object of type T
|
||||
// - Error: containing an error if validation or construction fails
|
||||
//
|
||||
// This allows builders to perform validation and return meaningful errors
|
||||
// during the construction process, making it explicit that object creation
|
||||
// may fail.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of object being built
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type ConfigBuilder struct {
|
||||
// host string
|
||||
// port int
|
||||
// }
|
||||
//
|
||||
// func (b ConfigBuilder) Build() result.Result[Config] {
|
||||
// if b.host == "" {
|
||||
// return result.Error[Config](errors.New("host is required"))
|
||||
// }
|
||||
// if b.port <= 0 || b.port > 65535 {
|
||||
// return result.Error[Config](errors.New("invalid port"))
|
||||
// }
|
||||
// return result.Of(Config{Host: b.host, Port: b.port})
|
||||
// }
|
||||
Builder[T any] interface {
|
||||
// Build constructs and validates an object of type T.
|
||||
//
|
||||
// Returns:
|
||||
// - Result[T]: A Result containing either the successfully built object
|
||||
// or an error if validation or construction fails.
|
||||
Build() Result[T]
|
||||
}
|
||||
)
|
||||
|
||||
374
v2/builder/builder_test.go
Normal file
374
v2/builder/builder_test.go
Normal file
@@ -0,0 +1,374 @@
|
||||
// Copyright (c) 2024 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Test types for demonstration
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
type PersonBuilder struct {
|
||||
name string
|
||||
age int
|
||||
}
|
||||
|
||||
func (b PersonBuilder) WithName(name string) PersonBuilder {
|
||||
b.name = name
|
||||
return b
|
||||
}
|
||||
|
||||
func (b PersonBuilder) WithAge(age int) PersonBuilder {
|
||||
b.age = age
|
||||
return b
|
||||
}
|
||||
|
||||
func (b PersonBuilder) Build() Result[Person] {
|
||||
if b.name == "" {
|
||||
return result.Left[Person](errors.New("name is required"))
|
||||
}
|
||||
if b.age < 0 {
|
||||
return result.Left[Person](errors.New("age must be non-negative"))
|
||||
}
|
||||
if b.age > 150 {
|
||||
return result.Left[Person](errors.New("age must be realistic"))
|
||||
}
|
||||
return result.Of(Person{Name: b.name, Age: b.age})
|
||||
}
|
||||
|
||||
func NewPersonBuilder(p Person) PersonBuilder {
|
||||
return PersonBuilder{name: p.Name, age: p.Age}
|
||||
}
|
||||
|
||||
// Config example for additional test coverage
|
||||
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
type ConfigBuilder struct {
|
||||
host string
|
||||
port int
|
||||
}
|
||||
|
||||
func (b ConfigBuilder) WithHost(host string) ConfigBuilder {
|
||||
b.host = host
|
||||
return b
|
||||
}
|
||||
|
||||
func (b ConfigBuilder) WithPort(port int) ConfigBuilder {
|
||||
b.port = port
|
||||
return b
|
||||
}
|
||||
|
||||
func (b ConfigBuilder) Build() Result[Config] {
|
||||
if b.host == "" {
|
||||
return result.Left[Config](errors.New("host is required"))
|
||||
}
|
||||
if b.port <= 0 || b.port > 65535 {
|
||||
return result.Left[Config](errors.New("port must be between 1 and 65535"))
|
||||
}
|
||||
return result.Of(Config{Host: b.host, Port: b.port})
|
||||
}
|
||||
|
||||
func NewConfigBuilder(c Config) ConfigBuilder {
|
||||
return ConfigBuilder{host: c.Host, port: c.Port}
|
||||
}
|
||||
|
||||
// Tests for Builder interface
|
||||
|
||||
func TestBuilder_SuccessfulBuild(t *testing.T) {
|
||||
builder := PersonBuilder{}.
|
||||
WithName("Alice").
|
||||
WithAge(30)
|
||||
|
||||
res := builder.Build()
|
||||
|
||||
assert.True(t, result.IsRight(res), "Build should succeed")
|
||||
person := result.ToOption(res)
|
||||
assert.True(t, O.IsSome(person), "Result should contain a person")
|
||||
|
||||
p := O.GetOrElse(func() Person { return Person{} })(person)
|
||||
assert.Equal(t, "Alice", p.Name)
|
||||
assert.Equal(t, 30, p.Age)
|
||||
}
|
||||
|
||||
func TestBuilder_ValidationFailure_MissingName(t *testing.T) {
|
||||
builder := PersonBuilder{}.WithAge(30)
|
||||
|
||||
res := builder.Build()
|
||||
|
||||
assert.True(t, result.IsLeft(res), "Build should fail when name is missing")
|
||||
err := result.Fold(
|
||||
func(e error) error { return e },
|
||||
func(Person) error { return errors.New("unexpected success") },
|
||||
)(res)
|
||||
assert.Equal(t, "name is required", err.Error())
|
||||
}
|
||||
|
||||
func TestBuilder_ValidationFailure_NegativeAge(t *testing.T) {
|
||||
builder := PersonBuilder{}.
|
||||
WithName("Bob").
|
||||
WithAge(-5)
|
||||
|
||||
res := builder.Build()
|
||||
|
||||
assert.True(t, result.IsLeft(res), "Build should fail when age is negative")
|
||||
err := result.Fold(
|
||||
func(e error) error { return e },
|
||||
func(Person) error { return errors.New("unexpected success") },
|
||||
)(res)
|
||||
assert.Equal(t, "age must be non-negative", err.Error())
|
||||
}
|
||||
|
||||
func TestBuilder_ValidationFailure_UnrealisticAge(t *testing.T) {
|
||||
builder := PersonBuilder{}.
|
||||
WithName("Charlie").
|
||||
WithAge(200)
|
||||
|
||||
res := builder.Build()
|
||||
|
||||
assert.True(t, result.IsLeft(res), "Build should fail when age is unrealistic")
|
||||
err := result.Fold(
|
||||
func(e error) error { return e },
|
||||
func(Person) error { return errors.New("unexpected success") },
|
||||
)(res)
|
||||
assert.Equal(t, "age must be realistic", err.Error())
|
||||
}
|
||||
|
||||
func TestBuilder_ConfigSuccessfulBuild(t *testing.T) {
|
||||
builder := ConfigBuilder{}.
|
||||
WithHost("localhost").
|
||||
WithPort(8080)
|
||||
|
||||
res := builder.Build()
|
||||
|
||||
assert.True(t, result.IsRight(res), "Build should succeed")
|
||||
config := result.ToOption(res)
|
||||
assert.True(t, O.IsSome(config), "Result should contain a config")
|
||||
|
||||
c := O.GetOrElse(func() Config { return Config{} })(config)
|
||||
assert.Equal(t, "localhost", c.Host)
|
||||
assert.Equal(t, 8080, c.Port)
|
||||
}
|
||||
|
||||
func TestBuilder_ConfigValidationFailure_MissingHost(t *testing.T) {
|
||||
builder := ConfigBuilder{}.WithPort(8080)
|
||||
|
||||
res := builder.Build()
|
||||
|
||||
assert.True(t, result.IsLeft(res), "Build should fail when host is missing")
|
||||
err := result.Fold(
|
||||
func(e error) error { return e },
|
||||
func(Config) error { return errors.New("unexpected success") },
|
||||
)(res)
|
||||
assert.Equal(t, "host is required", err.Error())
|
||||
}
|
||||
|
||||
func TestBuilder_ConfigValidationFailure_InvalidPort(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
port int
|
||||
}{
|
||||
{"zero port", 0},
|
||||
{"negative port", -1},
|
||||
{"port too large", 70000},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
builder := ConfigBuilder{}.
|
||||
WithHost("localhost").
|
||||
WithPort(tt.port)
|
||||
|
||||
res := builder.Build()
|
||||
|
||||
assert.True(t, result.IsLeft(res), "Build should fail for invalid port")
|
||||
err := result.Fold(
|
||||
func(e error) error { return e },
|
||||
func(Config) error { return errors.New("unexpected success") },
|
||||
)(res)
|
||||
assert.Equal(t, "port must be between 1 and 65535", err.Error())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Tests for BuilderPrism
|
||||
|
||||
func TestBuilderPrism_GetOption_ValidBuilder(t *testing.T) {
|
||||
prism := BuilderPrism(NewPersonBuilder)
|
||||
|
||||
builder := PersonBuilder{}.
|
||||
WithName("Alice").
|
||||
WithAge(30)
|
||||
|
||||
personOpt := prism.GetOption(builder)
|
||||
|
||||
assert.True(t, O.IsSome(personOpt), "GetOption should return Some for valid builder")
|
||||
person := O.GetOrElse(func() Person { return Person{} })(personOpt)
|
||||
assert.Equal(t, "Alice", person.Name)
|
||||
assert.Equal(t, 30, person.Age)
|
||||
}
|
||||
|
||||
func TestBuilderPrism_GetOption_InvalidBuilder(t *testing.T) {
|
||||
prism := BuilderPrism(NewPersonBuilder)
|
||||
|
||||
builder := PersonBuilder{}.WithAge(30) // Missing name
|
||||
|
||||
personOpt := prism.GetOption(builder)
|
||||
|
||||
assert.True(t, O.IsNone(personOpt), "GetOption should return None for invalid builder")
|
||||
}
|
||||
|
||||
func TestBuilderPrism_ReverseGet(t *testing.T) {
|
||||
prism := BuilderPrism(NewPersonBuilder)
|
||||
|
||||
person := Person{Name: "Bob", Age: 25}
|
||||
|
||||
builder := prism.ReverseGet(person)
|
||||
|
||||
assert.Equal(t, "Bob", builder.name)
|
||||
assert.Equal(t, 25, builder.age)
|
||||
|
||||
// Verify the builder can build the same person
|
||||
res := builder.Build()
|
||||
assert.True(t, result.IsRight(res), "Builder from ReverseGet should be valid")
|
||||
|
||||
rebuilt := O.GetOrElse(func() Person { return Person{} })(result.ToOption(res))
|
||||
assert.Equal(t, person, rebuilt)
|
||||
}
|
||||
|
||||
func TestBuilderPrism_RoundTrip_ValidBuilder(t *testing.T) {
|
||||
prism := BuilderPrism(NewPersonBuilder)
|
||||
|
||||
originalBuilder := PersonBuilder{}.
|
||||
WithName("Charlie").
|
||||
WithAge(35)
|
||||
|
||||
// Extract person from builder
|
||||
personOpt := prism.GetOption(originalBuilder)
|
||||
assert.True(t, O.IsSome(personOpt), "Should extract person from valid builder")
|
||||
|
||||
person := O.GetOrElse(func() Person { return Person{} })(personOpt)
|
||||
|
||||
// Reconstruct builder from person
|
||||
rebuiltBuilder := prism.ReverseGet(person)
|
||||
|
||||
// Verify the rebuilt builder produces the same person
|
||||
rebuiltRes := rebuiltBuilder.Build()
|
||||
assert.True(t, result.IsRight(rebuiltRes), "Rebuilt builder should be valid")
|
||||
|
||||
rebuiltPerson := O.GetOrElse(func() Person { return Person{} })(result.ToOption(rebuiltRes))
|
||||
assert.Equal(t, person, rebuiltPerson)
|
||||
}
|
||||
|
||||
func TestBuilderPrism_ConfigPrism(t *testing.T) {
|
||||
prism := BuilderPrism(NewConfigBuilder)
|
||||
|
||||
builder := ConfigBuilder{}.
|
||||
WithHost("example.com").
|
||||
WithPort(443)
|
||||
|
||||
configOpt := prism.GetOption(builder)
|
||||
|
||||
assert.True(t, O.IsSome(configOpt), "GetOption should return Some for valid config builder")
|
||||
config := O.GetOrElse(func() Config { return Config{} })(configOpt)
|
||||
assert.Equal(t, "example.com", config.Host)
|
||||
assert.Equal(t, 443, config.Port)
|
||||
}
|
||||
|
||||
func TestBuilderPrism_ConfigPrism_InvalidBuilder(t *testing.T) {
|
||||
prism := BuilderPrism(NewConfigBuilder)
|
||||
|
||||
builder := ConfigBuilder{}.WithPort(8080) // Missing host
|
||||
|
||||
configOpt := prism.GetOption(builder)
|
||||
|
||||
assert.True(t, O.IsNone(configOpt), "GetOption should return None for invalid config builder")
|
||||
}
|
||||
|
||||
func TestBuilderPrism_ConfigPrism_ReverseGet(t *testing.T) {
|
||||
prism := BuilderPrism(NewConfigBuilder)
|
||||
|
||||
config := Config{Host: "api.example.com", Port: 9000}
|
||||
|
||||
builder := prism.ReverseGet(config)
|
||||
|
||||
assert.Equal(t, "api.example.com", builder.host)
|
||||
assert.Equal(t, 9000, builder.port)
|
||||
|
||||
// Verify the builder can build the same config
|
||||
res := builder.Build()
|
||||
assert.True(t, result.IsRight(res), "Builder from ReverseGet should be valid")
|
||||
|
||||
rebuilt := O.GetOrElse(func() Config { return Config{} })(result.ToOption(res))
|
||||
assert.Equal(t, config, rebuilt)
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
|
||||
func BenchmarkBuilder_SuccessfulBuild(b *testing.B) {
|
||||
builder := PersonBuilder{}.
|
||||
WithName("Alice").
|
||||
WithAge(30)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = builder.Build()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBuilder_FailedBuild(b *testing.B) {
|
||||
builder := PersonBuilder{}.WithAge(30) // Missing name
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = builder.Build()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBuilderPrism_GetOption(b *testing.B) {
|
||||
prism := BuilderPrism(NewPersonBuilder)
|
||||
builder := PersonBuilder{}.
|
||||
WithName("Alice").
|
||||
WithAge(30)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = prism.GetOption(builder)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBuilderPrism_ReverseGet(b *testing.B) {
|
||||
prism := BuilderPrism(NewPersonBuilder)
|
||||
person := Person{Name: "Bob", Age: 25}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = prism.ReverseGet(person)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,18 @@
|
||||
// Copyright (c) 2024 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
@@ -6,7 +21,61 @@ import (
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// BuilderPrism createa a [Prism] that converts between a builder and its type
|
||||
// BuilderPrism creates a [Prism] that converts between a builder and its built type.
|
||||
//
|
||||
// A Prism is an optic that focuses on a case of a sum type, providing bidirectional
|
||||
// conversion with the possibility of failure. This function creates a prism that:
|
||||
// - Extracts: Attempts to build the object from the builder (may fail)
|
||||
// - Constructs: Creates a builder from a valid object (always succeeds)
|
||||
//
|
||||
// The extraction direction (builder -> object) uses the Build method and converts
|
||||
// the Result to an Option, where errors become None. The construction direction
|
||||
// (object -> builder) uses the provided creator function.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of the object being built
|
||||
// - B: The builder type that implements Builder[T]
|
||||
//
|
||||
// Parameters:
|
||||
// - creator: A function that creates a builder from a valid object of type T.
|
||||
// This function should initialize the builder with all fields from the object.
|
||||
//
|
||||
// Returns:
|
||||
// - Prism[B, T]: A prism that can convert between the builder and the built type.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// type PersonBuilder struct {
|
||||
// name string
|
||||
// age int
|
||||
// }
|
||||
//
|
||||
// func (b PersonBuilder) Build() result.Result[Person] {
|
||||
// if b.name == "" {
|
||||
// return result.Error[Person](errors.New("name required"))
|
||||
// }
|
||||
// return result.Of(Person{Name: b.name, Age: b.age})
|
||||
// }
|
||||
//
|
||||
// func NewPersonBuilder(p Person) PersonBuilder {
|
||||
// return PersonBuilder{name: p.Name, age: p.Age}
|
||||
// }
|
||||
//
|
||||
// // Create a prism for PersonBuilder
|
||||
// prism := BuilderPrism(NewPersonBuilder)
|
||||
//
|
||||
// // Use the prism to extract a Person from a valid builder
|
||||
// builder := PersonBuilder{name: "Alice", age: 30}
|
||||
// person := prism.GetOption(builder) // Some(Person{Name: "Alice", Age: 30})
|
||||
//
|
||||
// // Use the prism to create a builder from a Person
|
||||
// p := Person{Name: "Bob", Age: 25}
|
||||
// b := prism.ReverseGet(p) // PersonBuilder{name: "Bob", age: 25}
|
||||
func BuilderPrism[T any, B Builder[T]](creator func(T) B) Prism[B, T] {
|
||||
return prism.MakePrismWithName(F.Flow2(B.Build, result.ToOption[T]), creator, "BuilderPrism")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
650
v2/either/applicative_test.go
Normal file
650
v2/either/applicative_test.go
Normal file
@@ -0,0 +1,650 @@
|
||||
// Copyright (c) 2024 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package either
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestApplicativeOf tests the Of operation of the Applicative type class
|
||||
func TestApplicativeOf(t *testing.T) {
|
||||
app := Applicative[error, int, string]()
|
||||
|
||||
t.Run("wraps a value in Right context", func(t *testing.T) {
|
||||
result := app.Of(42)
|
||||
assert.True(t, IsRight(result))
|
||||
assert.Equal(t, 42, GetOrElse(func(error) int { return 0 })(result))
|
||||
})
|
||||
|
||||
t.Run("wraps string value", func(t *testing.T) {
|
||||
app := Applicative[error, string, int]()
|
||||
result := app.Of("hello")
|
||||
assert.True(t, IsRight(result))
|
||||
assert.Equal(t, "hello", GetOrElse(func(error) string { return "" })(result))
|
||||
})
|
||||
|
||||
t.Run("wraps zero value", func(t *testing.T) {
|
||||
result := app.Of(0)
|
||||
assert.True(t, IsRight(result))
|
||||
assert.Equal(t, 0, GetOrElse(func(error) int { return -1 })(result))
|
||||
})
|
||||
|
||||
t.Run("wraps nil pointer", func(t *testing.T) {
|
||||
app := Applicative[error, *int, *string]()
|
||||
var ptr *int = nil
|
||||
result := app.Of(ptr)
|
||||
assert.True(t, IsRight(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMap tests the Map operation of the Applicative type class
|
||||
func TestApplicativeMap(t *testing.T) {
|
||||
app := Applicative[error, int, int]()
|
||||
|
||||
t.Run("maps a function over Right value", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
eitherValue := app.Of(21)
|
||||
result := app.Map(double)(eitherValue)
|
||||
assert.True(t, IsRight(result))
|
||||
assert.Equal(t, 42, GetOrElse(func(error) int { return 0 })(result))
|
||||
})
|
||||
|
||||
t.Run("maps type conversion", func(t *testing.T) {
|
||||
app := Applicative[error, int, string]()
|
||||
eitherValue := app.Of(42)
|
||||
result := app.Map(strconv.Itoa)(eitherValue)
|
||||
assert.True(t, IsRight(result))
|
||||
assert.Equal(t, "42", GetOrElse(func(error) string { return "" })(result))
|
||||
})
|
||||
|
||||
t.Run("maps identity function", func(t *testing.T) {
|
||||
identity := func(x int) int { return x }
|
||||
eitherValue := app.Of(42)
|
||||
result := app.Map(identity)(eitherValue)
|
||||
assert.Equal(t, 42, GetOrElse(func(error) int { return 0 })(result))
|
||||
})
|
||||
|
||||
t.Run("preserves Left on map", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
eitherValue := Left[int](errors.New("error"))
|
||||
result := app.Map(double)(eitherValue)
|
||||
assert.True(t, IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("maps with utils.Double", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
app.Of(21),
|
||||
app.Map(utils.Double),
|
||||
)
|
||||
assert.Equal(t, 42, GetOrElse(func(error) int { return 0 })(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeAp tests the Ap operation of the standard Applicative (fail-fast)
|
||||
func TestApplicativeAp(t *testing.T) {
|
||||
app := Applicative[error, int, int]()
|
||||
|
||||
t.Run("applies wrapped function to wrapped value", func(t *testing.T) {
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
eitherFunc := Right[error](add(10))
|
||||
eitherValue := Right[error](32)
|
||||
result := app.Ap(eitherValue)(eitherFunc)
|
||||
assert.True(t, IsRight(result))
|
||||
assert.Equal(t, 42, GetOrElse(func(error) int { return 0 })(result))
|
||||
})
|
||||
|
||||
t.Run("fails fast when function is Left", func(t *testing.T) {
|
||||
err1 := errors.New("function error")
|
||||
eitherFunc := Left[func(int) int](err1)
|
||||
eitherValue := Right[error](42)
|
||||
result := app.Ap(eitherValue)(eitherFunc)
|
||||
assert.True(t, IsLeft(result))
|
||||
assert.Equal(t, err1, ToError(result))
|
||||
})
|
||||
|
||||
t.Run("fails fast when value is Left", func(t *testing.T) {
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
err2 := errors.New("value error")
|
||||
eitherFunc := Right[error](add(10))
|
||||
eitherValue := Left[int](err2)
|
||||
result := app.Ap(eitherValue)(eitherFunc)
|
||||
assert.True(t, IsLeft(result))
|
||||
assert.Equal(t, err2, ToError(result))
|
||||
})
|
||||
|
||||
t.Run("fails fast when both are Left - returns first error", func(t *testing.T) {
|
||||
err1 := errors.New("function error")
|
||||
err2 := errors.New("value error")
|
||||
eitherFunc := Left[func(int) int](err1)
|
||||
eitherValue := Left[int](err2)
|
||||
result := app.Ap(eitherValue)(eitherFunc)
|
||||
assert.True(t, IsLeft(result))
|
||||
// Should return the first error (function error)
|
||||
assert.Equal(t, err1, ToError(result))
|
||||
})
|
||||
|
||||
t.Run("applies with type conversion", func(t *testing.T) {
|
||||
toStringAndAppend := func(suffix string) func(int) string {
|
||||
return func(n int) string {
|
||||
return strconv.Itoa(n) + suffix
|
||||
}
|
||||
}
|
||||
eitherFunc := Right[error](toStringAndAppend("!"))
|
||||
eitherValue := Right[error](42)
|
||||
result := Ap[string](eitherValue)(eitherFunc)
|
||||
assert.Equal(t, "42!", GetOrElse(func(error) string { return "" })(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeVOf tests the Of operation of ApplicativeV
|
||||
func TestApplicativeVOf(t *testing.T) {
|
||||
sg := S.MakeSemigroup(func(a, b string) string { return a + "; " + b })
|
||||
app := ApplicativeV[string, int, string](sg)
|
||||
|
||||
t.Run("wraps a value in Right context", func(t *testing.T) {
|
||||
result := app.Of(42)
|
||||
assert.True(t, IsRight(result))
|
||||
assert.Equal(t, 42, GetOrElse(func(string) int { return 0 })(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeVMap tests the Map operation of ApplicativeV
|
||||
func TestApplicativeVMap(t *testing.T) {
|
||||
sg := S.MakeSemigroup(func(a, b string) string { return a + "; " + b })
|
||||
app := ApplicativeV[string, int, int](sg)
|
||||
|
||||
t.Run("maps a function over Right value", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
eitherValue := app.Of(21)
|
||||
result := app.Map(double)(eitherValue)
|
||||
assert.True(t, IsRight(result))
|
||||
assert.Equal(t, 42, GetOrElse(func(string) int { return 0 })(result))
|
||||
})
|
||||
|
||||
t.Run("preserves Left on map", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
eitherValue := Left[int]("error")
|
||||
result := app.Map(double)(eitherValue)
|
||||
assert.True(t, IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeVAp tests the Ap operation of ApplicativeV (validation with error accumulation)
|
||||
func TestApplicativeVAp(t *testing.T) {
|
||||
sg := S.MakeSemigroup(func(a, b string) string { return a + "; " + b })
|
||||
app := ApplicativeV[string, int, int](sg)
|
||||
|
||||
t.Run("applies wrapped function to wrapped value", func(t *testing.T) {
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
eitherFunc := Right[string](add(10))
|
||||
eitherValue := Right[string](32)
|
||||
result := app.Ap(eitherValue)(eitherFunc)
|
||||
assert.True(t, IsRight(result))
|
||||
assert.Equal(t, 42, GetOrElse(func(string) int { return 0 })(result))
|
||||
})
|
||||
|
||||
t.Run("returns Left when function is Left", func(t *testing.T) {
|
||||
eitherFunc := Left[func(int) int]("function error")
|
||||
eitherValue := Right[string](42)
|
||||
result := app.Ap(eitherValue)(eitherFunc)
|
||||
assert.True(t, IsLeft(result))
|
||||
leftValue := Fold(F.Identity[string], F.Constant1[int](""))(result)
|
||||
assert.Equal(t, "function error", leftValue)
|
||||
})
|
||||
|
||||
t.Run("returns Left when value is Left", func(t *testing.T) {
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
eitherFunc := Right[string](add(10))
|
||||
eitherValue := Left[int]("value error")
|
||||
result := app.Ap(eitherValue)(eitherFunc)
|
||||
assert.True(t, IsLeft(result))
|
||||
leftValue := Fold(F.Identity[string], F.Constant1[int](""))(result)
|
||||
assert.Equal(t, "value error", leftValue)
|
||||
})
|
||||
|
||||
t.Run("accumulates errors when both are Left", func(t *testing.T) {
|
||||
eitherFunc := Left[func(int) int]("function error")
|
||||
eitherValue := Left[int]("value error")
|
||||
result := app.Ap(eitherValue)(eitherFunc)
|
||||
assert.True(t, IsLeft(result))
|
||||
// Should combine both errors using the semigroup
|
||||
combined := Fold(F.Identity[string], F.Constant1[int](""))(result)
|
||||
assert.Equal(t, "function error; value error", combined)
|
||||
})
|
||||
|
||||
t.Run("accumulates multiple validation errors", func(t *testing.T) {
|
||||
type ValidationErrors []string
|
||||
sg := S.MakeSemigroup(func(a, b ValidationErrors) ValidationErrors {
|
||||
return append(append(ValidationErrors{}, a...), b...)
|
||||
})
|
||||
app := ApplicativeV[ValidationErrors, int, int](sg)
|
||||
|
||||
eitherFunc := Left[func(int) int](ValidationErrors{"error1", "error2"})
|
||||
eitherValue := Left[int](ValidationErrors{"error3", "error4"})
|
||||
result := app.Ap(eitherValue)(eitherFunc)
|
||||
assert.True(t, IsLeft(result))
|
||||
|
||||
errors := Fold(F.Identity[ValidationErrors], F.Constant1[int](ValidationErrors{}))(result)
|
||||
assert.Equal(t, ValidationErrors{"error1", "error2", "error3", "error4"}, errors)
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeLaws tests the applicative functor laws for standard Applicative
|
||||
func TestApplicativeLaws(t *testing.T) {
|
||||
app := Applicative[error, int, int]()
|
||||
|
||||
t.Run("identity law: Ap(Of(id))(v) = v", func(t *testing.T) {
|
||||
identity := func(x int) int { return x }
|
||||
v := app.Of(42)
|
||||
|
||||
left := app.Ap(v)(Of[error](identity))
|
||||
right := v
|
||||
|
||||
assert.Equal(t, GetOrElse(func(error) int { return 0 })(right),
|
||||
GetOrElse(func(error) int { return 0 })(left))
|
||||
})
|
||||
|
||||
t.Run("homomorphism law: Ap(Of(x))(Of(f)) = Of(f(x))", func(t *testing.T) {
|
||||
f := func(x int) int { return x * 2 }
|
||||
x := 21
|
||||
|
||||
left := app.Ap(app.Of(x))(Of[error](f))
|
||||
right := app.Of(f(x))
|
||||
|
||||
assert.Equal(t, GetOrElse(func(error) int { return 0 })(right),
|
||||
GetOrElse(func(error) int { return 0 })(left))
|
||||
})
|
||||
|
||||
t.Run("interchange law: Ap(Of(y))(u) = Ap(u)(Of(f => f(y)))", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
u := Of[error](double)
|
||||
y := 21
|
||||
|
||||
left := app.Ap(app.Of(y))(u)
|
||||
|
||||
// For interchange, we need to apply the value to the function
|
||||
// This test verifies the law holds for the applicative
|
||||
right := Map[error](func(f func(int) int) int { return f(y) })(u)
|
||||
|
||||
assert.Equal(t, GetOrElse(func(error) int { return 0 })(right),
|
||||
GetOrElse(func(error) int { return 0 })(left))
|
||||
})
|
||||
|
||||
t.Run("composition law", func(t *testing.T) {
|
||||
// For Either, we test a simpler version of composition
|
||||
f := func(x int) int { return x * 2 }
|
||||
g := func(x int) int { return x + 10 }
|
||||
x := 16
|
||||
|
||||
// Apply g then f
|
||||
left := F.Pipe2(
|
||||
app.Of(x),
|
||||
app.Map(g),
|
||||
app.Map(f),
|
||||
)
|
||||
|
||||
// Compose f and g, then apply
|
||||
composed := func(x int) int { return f(g(x)) }
|
||||
right := app.Map(composed)(app.Of(x))
|
||||
|
||||
assert.Equal(t, GetOrElse(func(error) int { return 0 })(right),
|
||||
GetOrElse(func(error) int { return 0 })(left))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeVLaws tests the applicative functor laws for ApplicativeV
|
||||
func TestApplicativeVLaws(t *testing.T) {
|
||||
sg := S.MakeSemigroup(func(a, b string) string { return a + "; " + b })
|
||||
app := ApplicativeV[string, int, int](sg)
|
||||
|
||||
t.Run("identity law: Ap(Of(id))(v) = v", func(t *testing.T) {
|
||||
identity := func(x int) int { return x }
|
||||
v := app.Of(42)
|
||||
|
||||
left := app.Ap(v)(Of[string](identity))
|
||||
right := v
|
||||
|
||||
assert.Equal(t, GetOrElse(func(string) int { return 0 })(right),
|
||||
GetOrElse(func(string) int { return 0 })(left))
|
||||
})
|
||||
|
||||
t.Run("homomorphism law: Ap(Of(x))(Of(f)) = Of(f(x))", func(t *testing.T) {
|
||||
f := func(x int) int { return x * 2 }
|
||||
x := 21
|
||||
|
||||
left := app.Ap(app.Of(x))(Of[string](f))
|
||||
right := app.Of(f(x))
|
||||
|
||||
assert.Equal(t, GetOrElse(func(string) int { return 0 })(right),
|
||||
GetOrElse(func(string) int { return 0 })(left))
|
||||
})
|
||||
|
||||
t.Run("interchange law: Ap(Of(y))(u) = Ap(u)(Of(f => f(y)))", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
u := Of[string](double)
|
||||
y := 21
|
||||
|
||||
left := app.Ap(app.Of(y))(u)
|
||||
|
||||
// For interchange, we need to apply the value to the function
|
||||
right := Map[string](func(f func(int) int) int { return f(y) })(u)
|
||||
|
||||
assert.Equal(t, GetOrElse(func(string) int { return 0 })(right),
|
||||
GetOrElse(func(string) int { return 0 })(left))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeComposition tests composition of applicative operations
|
||||
func TestApplicativeComposition(t *testing.T) {
|
||||
app := Applicative[error, int, int]()
|
||||
|
||||
t.Run("composes Map and Of", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
result := F.Pipe1(
|
||||
app.Of(21),
|
||||
app.Map(double),
|
||||
)
|
||||
assert.Equal(t, 42, GetOrElse(func(error) int { return 0 })(result))
|
||||
})
|
||||
|
||||
t.Run("composes multiple Map operations", func(t *testing.T) {
|
||||
app := Applicative[error, int, string]()
|
||||
double := func(x int) int { return x * 2 }
|
||||
toString := func(x int) string { return strconv.Itoa(x) }
|
||||
|
||||
result := F.Pipe2(
|
||||
app.Of(21),
|
||||
Map[error](double),
|
||||
app.Map(toString),
|
||||
)
|
||||
assert.Equal(t, "42", GetOrElse(func(error) string { return "" })(result))
|
||||
})
|
||||
|
||||
t.Run("composes Map and Ap", func(t *testing.T) {
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
|
||||
eitherFunc := F.Pipe1(
|
||||
app.Of(5),
|
||||
Map[error](add),
|
||||
)
|
||||
eitherValue := app.Of(16)
|
||||
|
||||
result := app.Ap(eitherValue)(eitherFunc)
|
||||
assert.Equal(t, 21, GetOrElse(func(error) int { return 0 })(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMultipleArguments tests applying functions with multiple arguments
|
||||
func TestApplicativeMultipleArguments(t *testing.T) {
|
||||
app := Applicative[error, int, int]()
|
||||
|
||||
t.Run("applies curried two-argument function", func(t *testing.T) {
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
|
||||
eitherFunc := F.Pipe1(
|
||||
app.Of(10),
|
||||
Map[error](add),
|
||||
)
|
||||
|
||||
result := app.Ap(app.Of(32))(eitherFunc)
|
||||
assert.Equal(t, 42, GetOrElse(func(error) int { return 0 })(result))
|
||||
})
|
||||
|
||||
t.Run("applies curried three-argument function", func(t *testing.T) {
|
||||
add3 := func(a int) func(int) func(int) int {
|
||||
return func(b int) func(int) int {
|
||||
return func(c int) int {
|
||||
return a + b + c
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eitherFunc1 := F.Pipe1(
|
||||
app.Of(10),
|
||||
Map[error](add3),
|
||||
)
|
||||
|
||||
eitherFunc2 := Ap[func(int) int](app.Of(20))(eitherFunc1)
|
||||
result := Ap[int](app.Of(12))(eitherFunc2)
|
||||
|
||||
assert.Equal(t, 42, GetOrElse(func(error) int { return 0 })(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeInstance tests that Applicative returns a valid instance
|
||||
func TestApplicativeInstance(t *testing.T) {
|
||||
t.Run("returns non-nil instance", func(t *testing.T) {
|
||||
app := Applicative[error, int, string]()
|
||||
assert.NotNil(t, app)
|
||||
})
|
||||
|
||||
t.Run("multiple calls return independent instances", func(t *testing.T) {
|
||||
app1 := Applicative[error, int, string]()
|
||||
app2 := Applicative[error, int, string]()
|
||||
|
||||
result1 := app1.Of(42)
|
||||
result2 := app2.Of(43)
|
||||
|
||||
assert.Equal(t, 42, GetOrElse(func(error) int { return 0 })(result1))
|
||||
assert.Equal(t, 43, GetOrElse(func(error) int { return 0 })(result2))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeVInstance tests that ApplicativeV returns a valid instance
|
||||
func TestApplicativeVInstance(t *testing.T) {
|
||||
sg := S.MakeSemigroup(func(a, b string) string { return a + "; " + b })
|
||||
|
||||
t.Run("returns non-nil instance", func(t *testing.T) {
|
||||
app := ApplicativeV[string, int, string](sg)
|
||||
assert.NotNil(t, app)
|
||||
})
|
||||
|
||||
t.Run("multiple calls return independent instances", func(t *testing.T) {
|
||||
app1 := ApplicativeV[string, int, string](sg)
|
||||
app2 := ApplicativeV[string, int, string](sg)
|
||||
|
||||
result1 := app1.Of(42)
|
||||
result2 := app2.Of(43)
|
||||
|
||||
assert.Equal(t, 42, GetOrElse(func(string) int { return 0 })(result1))
|
||||
assert.Equal(t, 43, GetOrElse(func(string) int { return 0 })(result2))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeWithDifferentTypes tests applicative with various type combinations
|
||||
func TestApplicativeWithDifferentTypes(t *testing.T) {
|
||||
t.Run("int to string", func(t *testing.T) {
|
||||
app := Applicative[error, int, string]()
|
||||
result := app.Map(strconv.Itoa)(app.Of(42))
|
||||
assert.Equal(t, "42", GetOrElse(func(error) string { return "" })(result))
|
||||
})
|
||||
|
||||
t.Run("string to int", func(t *testing.T) {
|
||||
app := Applicative[error, string, int]()
|
||||
toLength := func(s string) int { return len(s) }
|
||||
result := app.Map(toLength)(app.Of("hello"))
|
||||
assert.Equal(t, 5, GetOrElse(func(error) int { return 0 })(result))
|
||||
})
|
||||
|
||||
t.Run("bool to string", func(t *testing.T) {
|
||||
app := Applicative[error, bool, string]()
|
||||
toString := func(b bool) string {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
result := app.Map(toString)(app.Of(true))
|
||||
assert.Equal(t, "true", GetOrElse(func(error) string { return "" })(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeVFormValidationExample demonstrates a realistic form validation scenario
|
||||
func TestApplicativeVFormValidationExample(t *testing.T) {
|
||||
type ValidationErrors []string
|
||||
|
||||
sg := S.MakeSemigroup(func(a, b ValidationErrors) ValidationErrors {
|
||||
return append(append(ValidationErrors{}, a...), b...)
|
||||
})
|
||||
|
||||
validateName := func(name string) Either[ValidationErrors, string] {
|
||||
if len(name) < 3 {
|
||||
return Left[string](ValidationErrors{"Name must be at least 3 characters"})
|
||||
}
|
||||
return Right[ValidationErrors](name)
|
||||
}
|
||||
|
||||
validateAge := func(age int) Either[ValidationErrors, int] {
|
||||
if age < 18 {
|
||||
return Left[int](ValidationErrors{"Must be 18 or older"})
|
||||
}
|
||||
return Right[ValidationErrors](age)
|
||||
}
|
||||
|
||||
validateEmail := func(email string) Either[ValidationErrors, string] {
|
||||
if len(email) == 0 {
|
||||
return Left[string](ValidationErrors{"Email is required"})
|
||||
}
|
||||
return Right[ValidationErrors](email)
|
||||
}
|
||||
|
||||
t.Run("all validations pass", func(t *testing.T) {
|
||||
name := validateName("Alice")
|
||||
age := validateAge(25)
|
||||
email := validateEmail("alice@example.com")
|
||||
|
||||
// Verify all individual validations passed
|
||||
assert.True(t, IsRight(name))
|
||||
assert.True(t, IsRight(age))
|
||||
assert.True(t, IsRight(email))
|
||||
|
||||
// Combine validations - all pass
|
||||
result := F.Pipe2(
|
||||
name,
|
||||
Map[ValidationErrors](func(n string) string { return n }),
|
||||
Map[ValidationErrors](func(n string) string { return n + " validated" }),
|
||||
)
|
||||
|
||||
assert.True(t, IsRight(result))
|
||||
value := GetOrElse(func(ValidationErrors) string { return "" })(result)
|
||||
assert.Equal(t, "Alice validated", value)
|
||||
})
|
||||
|
||||
t.Run("all validations fail - accumulates all errors", func(t *testing.T) {
|
||||
name := validateName("ab")
|
||||
age := validateAge(16)
|
||||
email := validateEmail("")
|
||||
|
||||
// Manually combine errors using the semigroup
|
||||
var allErrors ValidationErrors
|
||||
if IsLeft(name) {
|
||||
allErrors = Fold(F.Identity[ValidationErrors], F.Constant1[string](ValidationErrors{}))(name)
|
||||
}
|
||||
if IsLeft(age) {
|
||||
ageErrors := Fold(F.Identity[ValidationErrors], F.Constant1[int](ValidationErrors{}))(age)
|
||||
allErrors = sg.Concat(allErrors, ageErrors)
|
||||
}
|
||||
if IsLeft(email) {
|
||||
emailErrors := Fold(F.Identity[ValidationErrors], F.Constant1[string](ValidationErrors{}))(email)
|
||||
allErrors = sg.Concat(allErrors, emailErrors)
|
||||
}
|
||||
|
||||
assert.Len(t, allErrors, 3)
|
||||
assert.Contains(t, allErrors, "Name must be at least 3 characters")
|
||||
assert.Contains(t, allErrors, "Must be 18 or older")
|
||||
assert.Contains(t, allErrors, "Email is required")
|
||||
})
|
||||
|
||||
t.Run("partial validation failure", func(t *testing.T) {
|
||||
name := validateName("Alice")
|
||||
age := validateAge(16)
|
||||
email := validateEmail("")
|
||||
|
||||
// Verify name passes
|
||||
assert.True(t, IsRight(name))
|
||||
|
||||
// Manually combine errors using the semigroup
|
||||
var allErrors ValidationErrors
|
||||
if IsLeft(age) {
|
||||
allErrors = Fold(F.Identity[ValidationErrors], F.Constant1[int](ValidationErrors{}))(age)
|
||||
}
|
||||
if IsLeft(email) {
|
||||
emailErrors := Fold(F.Identity[ValidationErrors], F.Constant1[string](ValidationErrors{}))(email)
|
||||
if len(allErrors) > 0 {
|
||||
allErrors = sg.Concat(allErrors, emailErrors)
|
||||
} else {
|
||||
allErrors = emailErrors
|
||||
}
|
||||
}
|
||||
|
||||
assert.Len(t, allErrors, 2)
|
||||
assert.Contains(t, allErrors, "Must be 18 or older")
|
||||
assert.Contains(t, allErrors, "Email is required")
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeVsApplicativeV demonstrates the difference between fail-fast and validation
|
||||
func TestApplicativeVsApplicativeV(t *testing.T) {
|
||||
t.Run("Applicative fails fast", func(t *testing.T) {
|
||||
app := Applicative[error, int, int]()
|
||||
|
||||
err1 := errors.New("error1")
|
||||
err2 := errors.New("error2")
|
||||
|
||||
eitherFunc := Left[func(int) int](err1)
|
||||
eitherValue := Left[int](err2)
|
||||
|
||||
result := app.Ap(eitherValue)(eitherFunc)
|
||||
|
||||
assert.True(t, IsLeft(result))
|
||||
// Only the first error is returned
|
||||
assert.Equal(t, err1, ToError(result))
|
||||
})
|
||||
|
||||
t.Run("ApplicativeV accumulates errors", func(t *testing.T) {
|
||||
sg := S.MakeSemigroup(func(a, b string) string { return a + "; " + b })
|
||||
app := ApplicativeV[string, int, int](sg)
|
||||
|
||||
eitherFunc := Left[func(int) int]("error1")
|
||||
eitherValue := Left[int]("error2")
|
||||
|
||||
result := app.Ap(eitherValue)(eitherFunc)
|
||||
|
||||
assert.True(t, IsLeft(result))
|
||||
// Both errors are accumulated
|
||||
combined := Fold(F.Identity[string], F.Constant1[int](""))(result)
|
||||
assert.Equal(t, "error1; error2", combined)
|
||||
})
|
||||
}
|
||||
@@ -570,3 +570,41 @@ func Flap[E, B, A any](a A) Operator[E, func(A) B, B] {
|
||||
func MonadAlt[E, A any](fa Either[E, A], that Lazy[Either[E, A]]) Either[E, A] {
|
||||
return MonadFold(fa, F.Ignore1of1[E](that), Of[E, A])
|
||||
}
|
||||
|
||||
// Zero returns the zero value of an [Either], which is a Right containing the zero value of type A.
|
||||
// This function is useful as an identity element in monoid operations or for creating an empty Either
|
||||
// in a Right state.
|
||||
//
|
||||
// The returned Either is always a Right value containing the zero value of type A. For reference types
|
||||
// (pointers, slices, maps, channels, functions, interfaces), the zero value is nil. For value types
|
||||
// (numbers, booleans, structs), it's the type's zero value.
|
||||
//
|
||||
// Important: Zero() returns the same value as the default initialization of Either[E, A].
|
||||
// When you declare `var e Either[E, A]` without initialization, it has the same value as Zero[E, A]().
|
||||
//
|
||||
// Note: This differs from creating a Left value, which would represent an error or failure state.
|
||||
// Zero always produces a successful (Right) state with a zero value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Zero Either with int value
|
||||
// e1 := either.Zero[error, int]() // Right(0)
|
||||
//
|
||||
// // Zero Either with string value
|
||||
// e2 := either.Zero[error, string]() // Right("")
|
||||
//
|
||||
// // Zero Either with pointer type
|
||||
// e3 := either.Zero[error, *int]() // Right(nil)
|
||||
//
|
||||
// // Zero equals default initialization
|
||||
// var defaultInit Either[error, int]
|
||||
// zero := either.Zero[error, int]()
|
||||
// assert.Equal(t, defaultInit, zero) // true
|
||||
//
|
||||
// // Verify it's a Right value
|
||||
// e := either.Zero[error, int]()
|
||||
// assert.True(t, either.IsRight(e)) // true
|
||||
// assert.False(t, either.IsLeft(e)) // false
|
||||
func Zero[E, A any]() Either[E, A] {
|
||||
return Either[E, A]{isLeft: false}
|
||||
}
|
||||
|
||||
@@ -119,3 +119,227 @@ func TestStringer(t *testing.T) {
|
||||
var s fmt.Stringer = &e
|
||||
assert.Equal(t, exp, s.String())
|
||||
}
|
||||
|
||||
// TestZeroWithIntegers tests Zero function with integer types
|
||||
func TestZeroWithIntegers(t *testing.T) {
|
||||
e := Zero[error, int]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
assert.False(t, IsLeft(e), "Zero should not create a Left value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Equal(t, 0, value, "Right value should be zero for int")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
}
|
||||
|
||||
// TestZeroWithStrings tests Zero function with string types
|
||||
func TestZeroWithStrings(t *testing.T) {
|
||||
e := Zero[error, string]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
assert.False(t, IsLeft(e), "Zero should not create a Left value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Equal(t, "", value, "Right value should be empty string")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
}
|
||||
|
||||
// TestZeroWithBooleans tests Zero function with boolean types
|
||||
func TestZeroWithBooleans(t *testing.T) {
|
||||
e := Zero[error, bool]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Equal(t, false, value, "Right value should be false for bool")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
}
|
||||
|
||||
// TestZeroWithFloats tests Zero function with float types
|
||||
func TestZeroWithFloats(t *testing.T) {
|
||||
e := Zero[error, float64]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Equal(t, 0.0, value, "Right value should be 0.0 for float64")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
}
|
||||
|
||||
// TestZeroWithPointers tests Zero function with pointer types
|
||||
func TestZeroWithPointers(t *testing.T) {
|
||||
e := Zero[error, *int]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Nil(t, value, "Right value should be nil for pointer type")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
}
|
||||
|
||||
// TestZeroWithSlices tests Zero function with slice types
|
||||
func TestZeroWithSlices(t *testing.T) {
|
||||
e := Zero[error, []int]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Nil(t, value, "Right value should be nil for slice type")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
}
|
||||
|
||||
// TestZeroWithMaps tests Zero function with map types
|
||||
func TestZeroWithMaps(t *testing.T) {
|
||||
e := Zero[error, map[string]int]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Nil(t, value, "Right value should be nil for map type")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
}
|
||||
|
||||
// TestZeroWithStructs tests Zero function with struct types
|
||||
func TestZeroWithStructs(t *testing.T) {
|
||||
type TestStruct struct {
|
||||
Field1 int
|
||||
Field2 string
|
||||
}
|
||||
|
||||
e := Zero[error, TestStruct]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
expected := TestStruct{Field1: 0, Field2: ""}
|
||||
assert.Equal(t, expected, value, "Right value should be zero value for struct")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
}
|
||||
|
||||
// TestZeroWithInterfaces tests Zero function with interface types
|
||||
func TestZeroWithInterfaces(t *testing.T) {
|
||||
e := Zero[error, interface{}]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Nil(t, value, "Right value should be nil for interface type")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
}
|
||||
|
||||
// TestZeroWithCustomErrorType tests Zero function with custom error types
|
||||
func TestZeroWithCustomErrorType(t *testing.T) {
|
||||
type CustomError struct {
|
||||
Code int
|
||||
Message string
|
||||
}
|
||||
|
||||
e := Zero[CustomError, string]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
assert.False(t, IsLeft(e), "Zero should not create a Left value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Equal(t, "", value, "Right value should be empty string")
|
||||
assert.Equal(t, CustomError{Code: 0, Message: ""}, err, "Error should be zero value for CustomError")
|
||||
}
|
||||
|
||||
// TestZeroCanBeUsedWithOtherFunctions tests that Zero Eithers work with other either functions
|
||||
func TestZeroCanBeUsedWithOtherFunctions(t *testing.T) {
|
||||
e := Zero[error, int]()
|
||||
|
||||
// Test with Map
|
||||
mapped := MonadMap(e, func(n int) string {
|
||||
return fmt.Sprintf("%d", n)
|
||||
})
|
||||
assert.True(t, IsRight(mapped), "Mapped Zero should still be Right")
|
||||
value, _ := Unwrap(mapped)
|
||||
assert.Equal(t, "0", value, "Mapped value should be '0'")
|
||||
|
||||
// Test with Chain
|
||||
chained := MonadChain(e, func(n int) Either[error, string] {
|
||||
return Right[error](fmt.Sprintf("value: %d", n))
|
||||
})
|
||||
assert.True(t, IsRight(chained), "Chained Zero should still be Right")
|
||||
chainedValue, _ := Unwrap(chained)
|
||||
assert.Equal(t, "value: 0", chainedValue, "Chained value should be 'value: 0'")
|
||||
|
||||
// Test with Fold
|
||||
folded := MonadFold(e,
|
||||
func(err error) string { return "error" },
|
||||
func(n int) string { return fmt.Sprintf("success: %d", n) },
|
||||
)
|
||||
assert.Equal(t, "success: 0", folded, "Folded value should be 'success: 0'")
|
||||
}
|
||||
|
||||
// TestZeroEquality tests that multiple Zero calls produce equal Eithers
|
||||
func TestZeroEquality(t *testing.T) {
|
||||
e1 := Zero[error, int]()
|
||||
e2 := Zero[error, int]()
|
||||
|
||||
assert.Equal(t, IsRight(e1), IsRight(e2), "Both should be Right")
|
||||
assert.Equal(t, IsLeft(e1), IsLeft(e2), "Both should not be Left")
|
||||
|
||||
v1, err1 := Unwrap(e1)
|
||||
v2, err2 := Unwrap(e2)
|
||||
assert.Equal(t, v1, v2, "Values should be equal")
|
||||
assert.Equal(t, err1, err2, "Errors should be equal")
|
||||
}
|
||||
|
||||
// TestZeroWithComplexTypes tests Zero with more complex nested types
|
||||
func TestZeroWithComplexTypes(t *testing.T) {
|
||||
type ComplexType struct {
|
||||
Nested map[string][]int
|
||||
Ptr *string
|
||||
}
|
||||
|
||||
e := Zero[error, ComplexType]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
expected := ComplexType{Nested: nil, Ptr: nil}
|
||||
assert.Equal(t, expected, value, "Right value should be zero value for complex struct")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
}
|
||||
|
||||
// TestZeroWithOption tests Zero with Option type
|
||||
func TestZeroWithOption(t *testing.T) {
|
||||
e := Zero[error, O.Option[int]]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.True(t, O.IsNone(value), "Right value should be None for Option type")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
}
|
||||
|
||||
// TestZeroIsNotLeft tests that Zero never creates a Left value
|
||||
func TestZeroIsNotLeft(t *testing.T) {
|
||||
// Test with various type combinations
|
||||
e1 := Zero[string, int]()
|
||||
e2 := Zero[error, string]()
|
||||
e3 := Zero[int, bool]()
|
||||
|
||||
assert.False(t, IsLeft(e1), "Zero should never create a Left value")
|
||||
assert.False(t, IsLeft(e2), "Zero should never create a Left value")
|
||||
assert.False(t, IsLeft(e3), "Zero should never create a Left value")
|
||||
|
||||
assert.True(t, IsRight(e1), "Zero should always create a Right value")
|
||||
assert.True(t, IsRight(e2), "Zero should always create a Right value")
|
||||
assert.True(t, IsRight(e3), "Zero should always create a Right value")
|
||||
}
|
||||
|
||||
// TestZeroEqualsDefaultInitialization tests that Zero returns the same value as default initialization
|
||||
func TestZeroEqualsDefaultInitialization(t *testing.T) {
|
||||
// Default initialization of Either
|
||||
var defaultInit Either[error, int]
|
||||
|
||||
// Zero function
|
||||
zero := Zero[error, int]()
|
||||
|
||||
// They should be equal
|
||||
assert.Equal(t, defaultInit, zero, "Zero should equal default initialization")
|
||||
assert.Equal(t, IsRight(defaultInit), IsRight(zero), "Both should be Right")
|
||||
assert.Equal(t, IsLeft(defaultInit), IsLeft(zero), "Both should not be Left")
|
||||
}
|
||||
|
||||
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))
|
||||
})
|
||||
}
|
||||
@@ -19,6 +19,64 @@ import (
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
|
||||
// TailRec converts a tail-recursive Kleisli arrow into a stack-safe iterative computation.
|
||||
//
|
||||
// This function enables writing recursive algorithms in a functional style while avoiding
|
||||
// stack overflow errors. It takes a Kleisli arrow that returns a Trampoline wrapped in Either,
|
||||
// and converts it into a regular Kleisli arrow that executes the recursion iteratively.
|
||||
//
|
||||
// The function handles both success and failure cases:
|
||||
// - If any step returns Left[E], the recursion stops and returns that error
|
||||
// - If a step returns Right with Landed=true, the final result is returned
|
||||
// - If a step returns Right with Landed=false, recursion continues with the bounced value
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The error type (Left case)
|
||||
// - A: The input type for each recursive step
|
||||
// - B: The final result type (Right case)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Kleisli arrow that takes an input of type A and returns Either[E, Trampoline[A, B]]
|
||||
// The Trampoline indicates whether to continue (Bounce) or terminate (Land)
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that executes the tail recursion iteratively and returns Either[E, B]
|
||||
//
|
||||
// Example - Factorial with error handling:
|
||||
//
|
||||
// type State struct { n, acc int }
|
||||
//
|
||||
// factorialStep := func(state State) either.Either[string, tailrec.Trampoline[State, int]] {
|
||||
// if state.n < 0 {
|
||||
// return either.Left[tailrec.Trampoline[State, int]]("negative input")
|
||||
// }
|
||||
// if state.n <= 1 {
|
||||
// return either.Right[string](tailrec.Land[State](state.acc))
|
||||
// }
|
||||
// return either.Right[string](tailrec.Bounce[int](State{state.n - 1, state.acc * state.n}))
|
||||
// }
|
||||
//
|
||||
// factorial := either.TailRec(factorialStep)
|
||||
// result := factorial(State{5, 1}) // Right(120)
|
||||
// error := factorial(State{-1, 1}) // Left("negative input")
|
||||
//
|
||||
// Example - Countdown with validation:
|
||||
//
|
||||
// countdown := either.TailRec(func(n int) either.Either[string, tailrec.Trampoline[int, int]] {
|
||||
// if n < 0 {
|
||||
// return either.Left[tailrec.Trampoline[int, int]]("already negative")
|
||||
// }
|
||||
// if n == 0 {
|
||||
// return either.Right[string](tailrec.Land[int](0))
|
||||
// }
|
||||
// return either.Right[string](tailrec.Bounce[int](n - 1))
|
||||
// })
|
||||
//
|
||||
// result := countdown(5) // Right(0)
|
||||
//
|
||||
// The function is stack-safe and can handle arbitrarily deep recursion without
|
||||
// causing stack overflow, as it uses iteration internally rather than actual recursion.
|
||||
//
|
||||
//go:inline
|
||||
func TailRec[E, A, B any](f Kleisli[E, A, tailrec.Trampoline[A, B]]) Kleisli[E, A, B] {
|
||||
return func(a A) Either[E, B] {
|
||||
|
||||
495
v2/either/rec_test.go
Normal file
495
v2/either/rec_test.go
Normal file
@@ -0,0 +1,495 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package either
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
TR "github.com/IBM/fp-go/v2/tailrec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestTailRecFactorial tests factorial computation with error handling
|
||||
func TestTailRecFactorial(t *testing.T) {
|
||||
type State struct {
|
||||
n int
|
||||
acc int
|
||||
}
|
||||
|
||||
factorialStep := func(state State) Either[string, TR.Trampoline[State, int]] {
|
||||
if state.n < 0 {
|
||||
return Left[TR.Trampoline[State, int]]("negative input not allowed")
|
||||
}
|
||||
if state.n <= 1 {
|
||||
return Right[string](TR.Land[State](state.acc))
|
||||
}
|
||||
return Right[string](TR.Bounce[int](State{state.n - 1, state.acc * state.n}))
|
||||
}
|
||||
|
||||
factorial := TailRec(factorialStep)
|
||||
|
||||
// Test successful computation
|
||||
result := factorial(State{5, 1})
|
||||
assert.Equal(t, Of[string](120), result)
|
||||
|
||||
// Test base case
|
||||
result = factorial(State{0, 1})
|
||||
assert.Equal(t, Of[string](1), result)
|
||||
|
||||
// Test error case
|
||||
result = factorial(State{-1, 1})
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := Unwrap(result)
|
||||
assert.Equal(t, "negative input not allowed", err)
|
||||
}
|
||||
|
||||
// TestTailRecFibonacci tests Fibonacci computation with validation
|
||||
func TestTailRecFibonacci(t *testing.T) {
|
||||
type State struct {
|
||||
n int
|
||||
prev int
|
||||
curr int
|
||||
}
|
||||
|
||||
fibStep := func(state State) Either[string, TR.Trampoline[State, int]] {
|
||||
if state.n < 0 {
|
||||
return Left[TR.Trampoline[State, int]]("negative index")
|
||||
}
|
||||
if state.curr > 1000 {
|
||||
return Left[TR.Trampoline[State, int]](fmt.Sprintf("value too large: %d", state.curr))
|
||||
}
|
||||
if state.n <= 0 {
|
||||
return Right[string](TR.Land[State](state.curr))
|
||||
}
|
||||
return Right[string](TR.Bounce[int](State{state.n - 1, state.curr, state.prev + state.curr}))
|
||||
}
|
||||
|
||||
fib := TailRec(fibStep)
|
||||
|
||||
// Test successful computation
|
||||
result := fib(State{10, 0, 1})
|
||||
assert.Equal(t, Of[string](89), result) // 10th Fibonacci number
|
||||
|
||||
// Test base case
|
||||
result = fib(State{0, 0, 1})
|
||||
assert.Equal(t, Of[string](1), result)
|
||||
|
||||
// Test error case - negative
|
||||
result = fib(State{-1, 0, 1})
|
||||
assert.True(t, IsLeft(result))
|
||||
|
||||
// Test error case - value too large
|
||||
result = fib(State{20, 0, 1})
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := Unwrap(result)
|
||||
assert.Contains(t, err, "value too large")
|
||||
}
|
||||
|
||||
// TestTailRecCountdown tests countdown with validation
|
||||
func TestTailRecCountdown(t *testing.T) {
|
||||
countdownStep := func(n int) Either[string, TR.Trampoline[int, int]] {
|
||||
if n < 0 {
|
||||
return Left[TR.Trampoline[int, int]]("already negative")
|
||||
}
|
||||
if n == 0 {
|
||||
return Right[string](TR.Land[int](0))
|
||||
}
|
||||
return Right[string](TR.Bounce[int](n - 1))
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
|
||||
// Test successful countdown
|
||||
result := countdown(10)
|
||||
assert.Equal(t, Of[string](0), result)
|
||||
|
||||
// Test immediate termination
|
||||
result = countdown(0)
|
||||
assert.Equal(t, Of[string](0), result)
|
||||
|
||||
// Test error case
|
||||
result = countdown(-5)
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := Unwrap(result)
|
||||
assert.Equal(t, "already negative", err)
|
||||
}
|
||||
|
||||
// TestTailRecSumList tests summing a list with error handling
|
||||
func TestTailRecSumList(t *testing.T) {
|
||||
type State struct {
|
||||
list []int
|
||||
sum int
|
||||
}
|
||||
|
||||
sumStep := func(state State) Either[string, TR.Trampoline[State, int]] {
|
||||
if state.sum > 100 {
|
||||
return Left[TR.Trampoline[State, int]](fmt.Sprintf("sum exceeds limit: %d", state.sum))
|
||||
}
|
||||
if A.IsEmpty(state.list) {
|
||||
return Right[string](TR.Land[State](state.sum))
|
||||
}
|
||||
return Right[string](TR.Bounce[int](State{state.list[1:], state.sum + state.list[0]}))
|
||||
}
|
||||
|
||||
sumList := TailRec(sumStep)
|
||||
|
||||
// Test successful sum
|
||||
result := sumList(State{[]int{1, 2, 3, 4, 5}, 0})
|
||||
assert.Equal(t, Of[string](15), result)
|
||||
|
||||
// Test empty list
|
||||
result = sumList(State{[]int{}, 0})
|
||||
assert.Equal(t, Of[string](0), result)
|
||||
|
||||
// Test error case - sum too large
|
||||
result = sumList(State{[]int{50, 60}, 0})
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := Unwrap(result)
|
||||
assert.Contains(t, err, "sum exceeds limit")
|
||||
}
|
||||
|
||||
// TestTailRecImmediateTermination tests immediate termination (Land on first call)
|
||||
func TestTailRecImmediateTermination(t *testing.T) {
|
||||
immediateStep := func(n int) Either[string, TR.Trampoline[int, int]] {
|
||||
return Right[string](TR.Land[int](n * 2))
|
||||
}
|
||||
|
||||
immediate := TailRec(immediateStep)
|
||||
result := immediate(21)
|
||||
|
||||
assert.Equal(t, Of[string](42), result)
|
||||
}
|
||||
|
||||
// TestTailRecImmediateError tests immediate error (Left on first call)
|
||||
func TestTailRecImmediateError(t *testing.T) {
|
||||
immediateErrorStep := func(n int) Either[string, TR.Trampoline[int, int]] {
|
||||
return Left[TR.Trampoline[int, int]]("immediate error")
|
||||
}
|
||||
|
||||
immediateError := TailRec(immediateErrorStep)
|
||||
result := immediateError(42)
|
||||
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := Unwrap(result)
|
||||
assert.Equal(t, "immediate error", err)
|
||||
}
|
||||
|
||||
// TestTailRecStackSafety tests that TailRec handles large iterations without stack overflow
|
||||
func TestTailRecStackSafety(t *testing.T) {
|
||||
countdownStep := func(n int) Either[string, TR.Trampoline[int, int]] {
|
||||
if n <= 0 {
|
||||
return Right[string](TR.Land[int](n))
|
||||
}
|
||||
return Right[string](TR.Bounce[int](n - 1))
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(10000)
|
||||
|
||||
assert.Equal(t, Of[string](0), result)
|
||||
}
|
||||
|
||||
// TestTailRecFindInRange tests finding a value in a range
|
||||
func TestTailRecFindInRange(t *testing.T) {
|
||||
type State struct {
|
||||
current int
|
||||
max int
|
||||
target int
|
||||
}
|
||||
|
||||
findStep := func(state State) Either[string, TR.Trampoline[State, int]] {
|
||||
if state.current > 1000 {
|
||||
return Left[TR.Trampoline[State, int]]("search exceeded maximum iterations")
|
||||
}
|
||||
if state.current >= state.max {
|
||||
return Right[string](TR.Land[State](-1)) // Not found
|
||||
}
|
||||
if state.current == state.target {
|
||||
return Right[string](TR.Land[State](state.current)) // Found
|
||||
}
|
||||
return Right[string](TR.Bounce[int](State{state.current + 1, state.max, state.target}))
|
||||
}
|
||||
|
||||
find := TailRec(findStep)
|
||||
|
||||
// Test found
|
||||
result := find(State{0, 100, 42})
|
||||
assert.Equal(t, Of[string](42), result)
|
||||
|
||||
// Test not found
|
||||
result = find(State{0, 100, 200})
|
||||
assert.Equal(t, Of[string](-1), result)
|
||||
|
||||
// Test error - exceeded iterations
|
||||
result = find(State{0, 2000, 1500})
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := Unwrap(result)
|
||||
assert.Contains(t, err, "exceeded maximum")
|
||||
}
|
||||
|
||||
// TestTailRecCollatzConjecture tests the Collatz conjecture
|
||||
func TestTailRecCollatzConjecture(t *testing.T) {
|
||||
collatzStep := func(n int) Either[string, TR.Trampoline[int, int]] {
|
||||
if n <= 0 {
|
||||
return Left[TR.Trampoline[int, int]]("invalid input: must be positive")
|
||||
}
|
||||
if n == 1 {
|
||||
return Right[string](TR.Land[int](1))
|
||||
}
|
||||
if n%2 == 0 {
|
||||
return Right[string](TR.Bounce[int](n / 2))
|
||||
}
|
||||
return Right[string](TR.Bounce[int](3*n + 1))
|
||||
}
|
||||
|
||||
collatz := TailRec(collatzStep)
|
||||
|
||||
// Test various starting points
|
||||
result := collatz(10)
|
||||
assert.Equal(t, Of[string](1), result)
|
||||
|
||||
result = collatz(27)
|
||||
assert.Equal(t, Of[string](1), result)
|
||||
|
||||
// Test error case
|
||||
result = collatz(0)
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := Unwrap(result)
|
||||
assert.Contains(t, err, "invalid input")
|
||||
}
|
||||
|
||||
// TestTailRecGCD tests greatest common divisor computation
|
||||
func TestTailRecGCD(t *testing.T) {
|
||||
type State struct {
|
||||
a int
|
||||
b int
|
||||
}
|
||||
|
||||
gcdStep := func(state State) Either[string, TR.Trampoline[State, int]] {
|
||||
if state.a < 0 || state.b < 0 {
|
||||
return Left[TR.Trampoline[State, int]]("negative values not allowed")
|
||||
}
|
||||
if state.b == 0 {
|
||||
return Right[string](TR.Land[State](state.a))
|
||||
}
|
||||
return Right[string](TR.Bounce[int](State{state.b, state.a % state.b}))
|
||||
}
|
||||
|
||||
gcd := TailRec(gcdStep)
|
||||
|
||||
// Test successful GCD
|
||||
result := gcd(State{48, 18})
|
||||
assert.Equal(t, Of[string](6), result)
|
||||
|
||||
result = gcd(State{100, 35})
|
||||
assert.Equal(t, Of[string](5), result)
|
||||
|
||||
// Test error case
|
||||
result = gcd(State{-10, 5})
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := Unwrap(result)
|
||||
assert.Contains(t, err, "negative values")
|
||||
}
|
||||
|
||||
// TestTailRecPowerOfTwo tests computing powers of 2
|
||||
func TestTailRecPowerOfTwo(t *testing.T) {
|
||||
type State struct {
|
||||
exponent int
|
||||
result int
|
||||
target int
|
||||
}
|
||||
|
||||
powerStep := func(state State) Either[string, TR.Trampoline[State, int]] {
|
||||
if state.target < 0 {
|
||||
return Left[TR.Trampoline[State, int]]("negative exponent not supported")
|
||||
}
|
||||
if state.exponent >= state.target {
|
||||
return Right[string](TR.Land[State](state.result))
|
||||
}
|
||||
return Right[string](TR.Bounce[int](State{state.exponent + 1, state.result * 2, state.target}))
|
||||
}
|
||||
|
||||
power := TailRec(powerStep)
|
||||
|
||||
// Test 2^10
|
||||
result := power(State{0, 1, 10})
|
||||
assert.Equal(t, Of[string](1024), result)
|
||||
|
||||
// Test 2^0
|
||||
result = power(State{0, 1, 0})
|
||||
assert.Equal(t, Of[string](1), result)
|
||||
|
||||
// Test error case
|
||||
result = power(State{0, 1, -1})
|
||||
assert.True(t, IsLeft(result))
|
||||
}
|
||||
|
||||
// TestTailRecErrorInMiddle tests error occurring in the middle of recursion
|
||||
func TestTailRecErrorInMiddle(t *testing.T) {
|
||||
countdownStep := func(n int) Either[string, TR.Trampoline[int, int]] {
|
||||
if n == 5 {
|
||||
return Left[TR.Trampoline[int, int]]("error at 5")
|
||||
}
|
||||
if n <= 0 {
|
||||
return Right[string](TR.Land[int](n))
|
||||
}
|
||||
return Right[string](TR.Bounce[int](n - 1))
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(10)
|
||||
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := Unwrap(result)
|
||||
assert.Equal(t, "error at 5", err)
|
||||
}
|
||||
|
||||
// TestTailRecMultipleErrorConditions tests multiple error conditions
|
||||
func TestTailRecMultipleErrorConditions(t *testing.T) {
|
||||
type State struct {
|
||||
value int
|
||||
steps int
|
||||
}
|
||||
|
||||
step := func(state State) Either[string, TR.Trampoline[State, int]] {
|
||||
if state.steps > 100 {
|
||||
return Left[TR.Trampoline[State, int]]("too many steps")
|
||||
}
|
||||
if state.value < 0 {
|
||||
return Left[TR.Trampoline[State, int]]("negative value encountered")
|
||||
}
|
||||
if state.value == 0 {
|
||||
return Right[string](TR.Land[State](state.steps))
|
||||
}
|
||||
return Right[string](TR.Bounce[int](State{state.value - 1, state.steps + 1}))
|
||||
}
|
||||
|
||||
counter := TailRec(step)
|
||||
|
||||
// Test successful case
|
||||
result := counter(State{10, 0})
|
||||
assert.Equal(t, Of[string](10), result)
|
||||
|
||||
// Test too many steps error
|
||||
result = counter(State{200, 0})
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := Unwrap(result)
|
||||
assert.Contains(t, err, "too many steps")
|
||||
}
|
||||
|
||||
// TestTailRecWithComplexState tests recursion with complex state
|
||||
func TestTailRecWithComplexState(t *testing.T) {
|
||||
type State struct {
|
||||
numbers []int
|
||||
sum int
|
||||
product int
|
||||
}
|
||||
|
||||
processStep := func(state State) Either[string, TR.Trampoline[State, State]] {
|
||||
if state.product > 10000 {
|
||||
return Left[TR.Trampoline[State, State]]("product overflow")
|
||||
}
|
||||
if A.IsEmpty(state.numbers) {
|
||||
return Right[string](TR.Land[State](state))
|
||||
}
|
||||
head := state.numbers[0]
|
||||
tail := state.numbers[1:]
|
||||
return Right[string](TR.Bounce[State](State{
|
||||
numbers: tail,
|
||||
sum: state.sum + head,
|
||||
product: state.product * head,
|
||||
}))
|
||||
}
|
||||
|
||||
process := TailRec(processStep)
|
||||
|
||||
// Test successful processing
|
||||
result := process(State{[]int{2, 3, 4}, 0, 1})
|
||||
assert.True(t, IsRight(result))
|
||||
finalState, _ := Unwrap(result)
|
||||
assert.Equal(t, 9, finalState.sum)
|
||||
assert.Equal(t, 24, finalState.product)
|
||||
|
||||
// Test overflow error
|
||||
result = process(State{[]int{100, 200, 300}, 0, 1})
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := Unwrap(result)
|
||||
assert.Contains(t, err, "product overflow")
|
||||
}
|
||||
|
||||
// TestTailRecDivisionByZeroProtection tests protection against division by zero
|
||||
func TestTailRecDivisionByZeroProtection(t *testing.T) {
|
||||
type State struct {
|
||||
numerator int
|
||||
denominator int
|
||||
result int
|
||||
}
|
||||
|
||||
divideStep := func(state State) Either[string, TR.Trampoline[State, int]] {
|
||||
if state.denominator == 0 {
|
||||
return Left[TR.Trampoline[State, int]]("division by zero")
|
||||
}
|
||||
if state.numerator < state.denominator {
|
||||
return Right[string](TR.Land[State](state.result))
|
||||
}
|
||||
return Right[string](TR.Bounce[int](State{
|
||||
numerator: state.numerator - state.denominator,
|
||||
denominator: state.denominator,
|
||||
result: state.result + 1,
|
||||
}))
|
||||
}
|
||||
|
||||
divide := TailRec(divideStep)
|
||||
|
||||
// Test successful division
|
||||
result := divide(State{10, 3, 0})
|
||||
assert.Equal(t, Of[string](3), result) // 10 / 3 = 3 (integer division)
|
||||
|
||||
// Test division by zero
|
||||
result = divide(State{10, 0, 0})
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := Unwrap(result)
|
||||
assert.Equal(t, "division by zero", err)
|
||||
}
|
||||
|
||||
// TestTailRecStringProcessing tests recursion with string processing
|
||||
func TestTailRecStringProcessing(t *testing.T) {
|
||||
type State struct {
|
||||
remaining string
|
||||
count int
|
||||
}
|
||||
|
||||
countVowels := func(state State) Either[string, TR.Trampoline[State, int]] {
|
||||
if len(state.remaining) == 0 {
|
||||
return Right[string](TR.Land[State](state.count))
|
||||
}
|
||||
char := state.remaining[0]
|
||||
isVowel := char == 'a' || char == 'e' || char == 'i' || char == 'o' || char == 'u' ||
|
||||
char == 'A' || char == 'E' || char == 'I' || char == 'O' || char == 'U'
|
||||
newCount := state.count
|
||||
if isVowel {
|
||||
newCount++
|
||||
}
|
||||
return Right[string](TR.Bounce[int](State{state.remaining[1:], newCount}))
|
||||
}
|
||||
|
||||
counter := TailRec(countVowels)
|
||||
|
||||
result := counter(State{"hello world", 0})
|
||||
assert.Equal(t, Of[string](3), result) // e, o, o
|
||||
}
|
||||
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]
|
||||
)
|
||||
492
v2/function/bind_test.go
Normal file
492
v2/function/bind_test.go
Normal file
@@ -0,0 +1,492 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package function
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestBind1st tests the Bind1st function with various scenarios
|
||||
func TestBind1st(t *testing.T) {
|
||||
t.Run("binds first parameter of multiplication", func(t *testing.T) {
|
||||
multiply := func(a, b int) int { return a * b }
|
||||
double := Bind1st(multiply, 2)
|
||||
triple := Bind1st(multiply, 3)
|
||||
|
||||
assert.Equal(t, 10, double(5))
|
||||
assert.Equal(t, 20, double(10))
|
||||
assert.Equal(t, 15, triple(5))
|
||||
assert.Equal(t, 30, triple(10))
|
||||
})
|
||||
|
||||
t.Run("binds first parameter of division", func(t *testing.T) {
|
||||
divide := func(a, b float64) float64 { return a / b }
|
||||
divideBy10 := Bind1st(divide, 10.0)
|
||||
divideBy5 := Bind1st(divide, 5.0)
|
||||
|
||||
assert.Equal(t, 5.0, divideBy10(2.0))
|
||||
assert.Equal(t, 2.0, divideBy10(5.0))
|
||||
assert.Equal(t, 1.0, divideBy5(5.0))
|
||||
})
|
||||
|
||||
t.Run("binds first parameter of subtraction", func(t *testing.T) {
|
||||
subtract := func(a, b int) int { return a - b }
|
||||
subtract10From := Bind1st(subtract, 10)
|
||||
|
||||
assert.Equal(t, 7, subtract10From(3)) // 10 - 3
|
||||
assert.Equal(t, 0, subtract10From(10)) // 10 - 10
|
||||
assert.Equal(t, -5, subtract10From(15)) // 10 - 15
|
||||
})
|
||||
|
||||
t.Run("binds first parameter of string concatenation", func(t *testing.T) {
|
||||
concat := func(a, b string) string { return a + b }
|
||||
addHello := Bind1st(concat, "Hello ")
|
||||
addPrefix := Bind1st(concat, "Prefix: ")
|
||||
|
||||
assert.Equal(t, "Hello World", addHello("World"))
|
||||
assert.Equal(t, "Hello Go", addHello("Go"))
|
||||
assert.Equal(t, "Prefix: Test", addPrefix("Test"))
|
||||
})
|
||||
|
||||
t.Run("binds first parameter with different types", func(t *testing.T) {
|
||||
repeat := func(s string, n int) string {
|
||||
return strings.Repeat(s, n)
|
||||
}
|
||||
repeatX := Bind1st(repeat, "x")
|
||||
repeatAB := Bind1st(repeat, "ab")
|
||||
|
||||
assert.Equal(t, "xxx", repeatX(3))
|
||||
assert.Equal(t, "xxxxx", repeatX(5))
|
||||
assert.Equal(t, "abab", repeatAB(2))
|
||||
})
|
||||
|
||||
t.Run("binds first parameter with complex types", func(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
format := func(p Person, suffix string) string {
|
||||
return fmt.Sprintf("%s (%d) %s", p.Name, p.Age, suffix)
|
||||
}
|
||||
|
||||
alice := Person{Name: "Alice", Age: 30}
|
||||
formatAlice := Bind1st(format, alice)
|
||||
|
||||
assert.Equal(t, "Alice (30) is here", formatAlice("is here"))
|
||||
assert.Equal(t, "Alice (30) says hello", formatAlice("says hello"))
|
||||
})
|
||||
|
||||
t.Run("binds first parameter with slice operations", func(t *testing.T) {
|
||||
appendSlice := func(slice []int, elem int) []int {
|
||||
return append(slice, elem)
|
||||
}
|
||||
|
||||
nums := []int{1, 2, 3}
|
||||
appendToNums := Bind1st(appendSlice, nums)
|
||||
|
||||
result1 := appendToNums(4)
|
||||
assert.Equal(t, []int{1, 2, 3, 4}, result1)
|
||||
|
||||
result2 := appendToNums(5)
|
||||
assert.Equal(t, []int{1, 2, 3, 5}, result2)
|
||||
})
|
||||
|
||||
t.Run("binds first parameter with map operations", func(t *testing.T) {
|
||||
getFromMap := func(m map[string]int, key string) int {
|
||||
return m[key]
|
||||
}
|
||||
|
||||
data := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
getFromData := Bind1st(getFromMap, data)
|
||||
|
||||
assert.Equal(t, 1, getFromData("a"))
|
||||
assert.Equal(t, 2, getFromData("b"))
|
||||
assert.Equal(t, 3, getFromData("c"))
|
||||
})
|
||||
|
||||
t.Run("creates specialized comparison functions", func(t *testing.T) {
|
||||
greaterThan := func(a, b int) bool { return a > b }
|
||||
greaterThan10 := Bind1st(greaterThan, 10)
|
||||
greaterThan5 := Bind1st(greaterThan, 5)
|
||||
|
||||
assert.True(t, greaterThan10(3)) // 10 > 3
|
||||
assert.False(t, greaterThan10(15)) // 10 > 15
|
||||
assert.True(t, greaterThan5(3)) // 5 > 3
|
||||
assert.False(t, greaterThan5(10)) // 5 > 10
|
||||
})
|
||||
}
|
||||
|
||||
// TestBind2nd tests the Bind2nd function with various scenarios
|
||||
func TestBind2nd(t *testing.T) {
|
||||
t.Run("binds second parameter of multiplication", func(t *testing.T) {
|
||||
multiply := func(a, b int) int { return a * b }
|
||||
double := Bind2nd(multiply, 2)
|
||||
triple := Bind2nd(multiply, 3)
|
||||
|
||||
assert.Equal(t, 10, double(5))
|
||||
assert.Equal(t, 20, double(10))
|
||||
assert.Equal(t, 15, triple(5))
|
||||
assert.Equal(t, 30, triple(10))
|
||||
})
|
||||
|
||||
t.Run("binds second parameter of division", func(t *testing.T) {
|
||||
divide := func(a, b float64) float64 { return a / b }
|
||||
halve := Bind2nd(divide, 2.0)
|
||||
third := Bind2nd(divide, 3.0)
|
||||
|
||||
assert.Equal(t, 5.0, halve(10.0))
|
||||
assert.Equal(t, 2.5, halve(5.0))
|
||||
assert.InDelta(t, 3.333, third(10.0), 0.001)
|
||||
})
|
||||
|
||||
t.Run("binds second parameter of subtraction", func(t *testing.T) {
|
||||
subtract := func(a, b int) int { return a - b }
|
||||
decrementBy5 := Bind2nd(subtract, 5)
|
||||
decrementBy10 := Bind2nd(subtract, 10)
|
||||
|
||||
assert.Equal(t, 5, decrementBy5(10)) // 10 - 5
|
||||
assert.Equal(t, 0, decrementBy5(5)) // 5 - 5
|
||||
assert.Equal(t, 0, decrementBy10(10)) // 10 - 10
|
||||
assert.Equal(t, -5, decrementBy10(5)) // 5 - 10
|
||||
})
|
||||
|
||||
t.Run("binds second parameter of string concatenation", func(t *testing.T) {
|
||||
concat := func(a, b string) string { return a + b }
|
||||
addWorld := Bind2nd(concat, " World")
|
||||
addSuffix := Bind2nd(concat, "!")
|
||||
|
||||
assert.Equal(t, "Hello World", addWorld("Hello"))
|
||||
assert.Equal(t, "Goodbye World", addWorld("Goodbye"))
|
||||
assert.Equal(t, "Hello!", addSuffix("Hello"))
|
||||
})
|
||||
|
||||
t.Run("binds second parameter with different types", func(t *testing.T) {
|
||||
repeat := func(s string, n int) string {
|
||||
return strings.Repeat(s, n)
|
||||
}
|
||||
repeatThrice := Bind2nd(repeat, 3)
|
||||
repeatTwice := Bind2nd(repeat, 2)
|
||||
|
||||
assert.Equal(t, "xxx", repeatThrice("x"))
|
||||
assert.Equal(t, "ababab", repeatThrice("ab"))
|
||||
assert.Equal(t, "aa", repeatTwice("a"))
|
||||
})
|
||||
|
||||
t.Run("binds second parameter with complex types", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Debug bool
|
||||
Port int
|
||||
}
|
||||
|
||||
format := func(name string, cfg Config) string {
|
||||
return fmt.Sprintf("%s: debug=%v, port=%d", name, cfg.Debug, cfg.Port)
|
||||
}
|
||||
|
||||
prodConfig := Config{Debug: false, Port: 8080}
|
||||
formatWithProd := Bind2nd(format, prodConfig)
|
||||
|
||||
assert.Equal(t, "API: debug=false, port=8080", formatWithProd("API"))
|
||||
assert.Equal(t, "Web: debug=false, port=8080", formatWithProd("Web"))
|
||||
})
|
||||
|
||||
t.Run("binds second parameter with slice operations", func(t *testing.T) {
|
||||
appendElem := func(slice []int, elem int) []int {
|
||||
return append(slice, elem)
|
||||
}
|
||||
|
||||
append5 := Bind2nd(appendElem, 5)
|
||||
|
||||
result1 := append5([]int{1, 2, 3})
|
||||
assert.Equal(t, []int{1, 2, 3, 5}, result1)
|
||||
|
||||
result2 := append5([]int{10, 20})
|
||||
assert.Equal(t, []int{10, 20, 5}, result2)
|
||||
})
|
||||
|
||||
t.Run("creates specialized comparison functions", func(t *testing.T) {
|
||||
greaterThan := func(a, b int) bool { return a > b }
|
||||
greaterThan10 := Bind2nd(greaterThan, 10)
|
||||
greaterThan5 := Bind2nd(greaterThan, 5)
|
||||
|
||||
assert.False(t, greaterThan10(3)) // 3 > 10
|
||||
assert.True(t, greaterThan10(15)) // 15 > 10
|
||||
assert.False(t, greaterThan5(3)) // 3 > 5
|
||||
assert.True(t, greaterThan5(10)) // 10 > 5
|
||||
})
|
||||
|
||||
t.Run("binds second parameter for power function", func(t *testing.T) {
|
||||
power := func(base, exp float64) float64 {
|
||||
result := 1.0
|
||||
for i := 0; i < int(exp); i++ {
|
||||
result *= base
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
square := Bind2nd(power, 2.0)
|
||||
cube := Bind2nd(power, 3.0)
|
||||
|
||||
assert.Equal(t, 25.0, square(5.0))
|
||||
assert.Equal(t, 100.0, square(10.0))
|
||||
assert.Equal(t, 125.0, cube(5.0))
|
||||
assert.Equal(t, 8.0, cube(2.0))
|
||||
})
|
||||
}
|
||||
|
||||
// TestBind1stVsBind2nd tests the difference between Bind1st and Bind2nd
|
||||
func TestBind1stVsBind2nd(t *testing.T) {
|
||||
t.Run("demonstrates difference with non-commutative operations", func(t *testing.T) {
|
||||
subtract := func(a, b int) int { return a - b }
|
||||
|
||||
// Bind1st: fixes first parameter (a)
|
||||
subtract10From := Bind1st(subtract, 10) // 10 - b
|
||||
assert.Equal(t, 7, subtract10From(3)) // 10 - 3 = 7
|
||||
|
||||
// Bind2nd: fixes second parameter (b)
|
||||
decrementBy10 := Bind2nd(subtract, 10) // a - 10
|
||||
assert.Equal(t, -7, decrementBy10(3)) // 3 - 10 = -7
|
||||
})
|
||||
|
||||
t.Run("demonstrates difference with division", func(t *testing.T) {
|
||||
divide := func(a, b float64) float64 { return a / b }
|
||||
|
||||
// Bind1st: fixes numerator
|
||||
divide10By := Bind1st(divide, 10.0) // 10 / b
|
||||
assert.Equal(t, 5.0, divide10By(2.0)) // 10 / 2 = 5
|
||||
|
||||
// Bind2nd: fixes denominator
|
||||
divideBy10 := Bind2nd(divide, 10.0) // a / 10
|
||||
assert.Equal(t, 0.2, divideBy10(2.0)) // 2 / 10 = 0.2
|
||||
})
|
||||
|
||||
t.Run("demonstrates equivalence with commutative operations", func(t *testing.T) {
|
||||
add := func(a, b int) int { return a + b }
|
||||
|
||||
// For commutative operations, both should give same result
|
||||
add5First := Bind1st(add, 5) // 5 + b
|
||||
add5Second := Bind2nd(add, 5) // a + 5
|
||||
|
||||
assert.Equal(t, 8, add5First(3))
|
||||
assert.Equal(t, 8, add5Second(3))
|
||||
assert.Equal(t, add5First(10), add5Second(10))
|
||||
})
|
||||
}
|
||||
|
||||
// TestSK tests the SK combinator function
|
||||
func TestSK(t *testing.T) {
|
||||
t.Run("returns second argument ignoring first", func(t *testing.T) {
|
||||
assert.Equal(t, "hello", SK(42, "hello"))
|
||||
assert.Equal(t, 100, SK(true, 100))
|
||||
assert.Equal(t, 3.14, SK("test", 3.14))
|
||||
assert.Equal(t, false, SK(123, false))
|
||||
})
|
||||
|
||||
t.Run("works with nil values", func(t *testing.T) {
|
||||
var nilPtr *int
|
||||
assert.Nil(t, SK("ignored", nilPtr))
|
||||
assert.Equal(t, 42, SK(nilPtr, 42))
|
||||
})
|
||||
|
||||
t.Run("works with complex types", func(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
alice := Person{Name: "Alice", Age: 30}
|
||||
bob := Person{Name: "Bob", Age: 25}
|
||||
|
||||
result := SK(alice, bob)
|
||||
assert.Equal(t, "Bob", result.Name)
|
||||
assert.Equal(t, 25, result.Age)
|
||||
})
|
||||
|
||||
t.Run("works with slices", func(t *testing.T) {
|
||||
slice1 := []int{1, 2, 3}
|
||||
slice2 := []string{"a", "b", "c"}
|
||||
|
||||
result := SK(slice1, slice2)
|
||||
assert.Equal(t, []string{"a", "b", "c"}, result)
|
||||
})
|
||||
|
||||
t.Run("works with maps", func(t *testing.T) {
|
||||
map1 := map[string]int{"a": 1}
|
||||
map2 := map[int]string{1: "one"}
|
||||
|
||||
result := SK(map1, map2)
|
||||
assert.Equal(t, map[int]string{1: "one"}, result)
|
||||
})
|
||||
|
||||
t.Run("behaves identically to Second", func(t *testing.T) {
|
||||
// SK should be identical to Second function
|
||||
testCases := []struct {
|
||||
first any
|
||||
second any
|
||||
}{
|
||||
{42, "hello"},
|
||||
{true, 100},
|
||||
{"test", 3.14},
|
||||
{[]int{1, 2}, []string{"a", "b"}},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
assert.Equal(t,
|
||||
Second(tc.first, tc.second),
|
||||
SK(tc.first, tc.second),
|
||||
"SK should behave like Second")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("demonstrates K combinator property", func(t *testing.T) {
|
||||
// SK is the K combinator applied to the second argument
|
||||
// K x y = x, so SK x y = K y x = y
|
||||
// This means SK always returns its second argument
|
||||
|
||||
// Test with various types
|
||||
assert.Equal(t, 42, SK("anything", 42))
|
||||
assert.Equal(t, "result", SK(999, "result"))
|
||||
assert.True(t, SK(false, true))
|
||||
})
|
||||
}
|
||||
|
||||
// TestBindComposition tests composition of bind operations
|
||||
func TestBindComposition(t *testing.T) {
|
||||
t.Run("composes multiple Bind1st operations", func(t *testing.T) {
|
||||
add := func(a, b int) int { return a + b }
|
||||
multiply := func(a, b int) int { return a * b }
|
||||
|
||||
add5 := Bind1st(add, 5)
|
||||
double := Bind1st(multiply, 2)
|
||||
|
||||
// Compose: first add 5, then double
|
||||
result := double(add5(3)) // (3 + 5) * 2 = 16
|
||||
assert.Equal(t, 16, result)
|
||||
})
|
||||
|
||||
t.Run("composes Bind1st and Bind2nd", func(t *testing.T) {
|
||||
subtract := func(a, b int) int { return a - b }
|
||||
|
||||
subtract10From := Bind1st(subtract, 10) // 10 - b
|
||||
decrementBy5 := Bind2nd(subtract, 5) // a - 5
|
||||
|
||||
// Apply both transformations
|
||||
result1 := decrementBy5(subtract10From(3)) // (10 - 3) - 5 = 2
|
||||
assert.Equal(t, 2, result1)
|
||||
|
||||
result2 := subtract10From(decrementBy5(8)) // 10 - (8 - 5) = 7
|
||||
assert.Equal(t, 7, result2)
|
||||
})
|
||||
|
||||
t.Run("creates pipeline with bound functions", func(t *testing.T) {
|
||||
multiply := func(a, b int) int { return a * b }
|
||||
add := func(a, b int) int { return a + b }
|
||||
|
||||
double := Bind2nd(multiply, 2)
|
||||
add10 := Bind2nd(add, 10)
|
||||
|
||||
// Pipeline: input -> double -> add10
|
||||
pipeline := func(n int) int {
|
||||
return add10(double(n))
|
||||
}
|
||||
|
||||
assert.Equal(t, 20, pipeline(5)) // (5 * 2) + 10 = 20
|
||||
assert.Equal(t, 30, pipeline(10)) // (10 * 2) + 10 = 30
|
||||
})
|
||||
}
|
||||
|
||||
// TestBindWithHigherOrderFunctions tests bind with higher-order functions
|
||||
func TestBindWithHigherOrderFunctions(t *testing.T) {
|
||||
t.Run("binds function parameter", func(t *testing.T) {
|
||||
applyTwice := func(f func(int) int, n int) int {
|
||||
return f(f(n))
|
||||
}
|
||||
|
||||
increment := func(n int) int { return n + 1 }
|
||||
applyIncrementTwice := Bind1st(applyTwice, increment)
|
||||
|
||||
assert.Equal(t, 7, applyIncrementTwice(5)) // increment(increment(5)) = 7
|
||||
})
|
||||
|
||||
t.Run("binds value for higher-order function", func(t *testing.T) {
|
||||
applyFunc := func(f func(int) int, n int) int {
|
||||
return f(n)
|
||||
}
|
||||
|
||||
applyTo10 := Bind2nd(applyFunc, 10)
|
||||
|
||||
double := func(n int) int { return n * 2 }
|
||||
square := func(n int) int { return n * n }
|
||||
|
||||
assert.Equal(t, 20, applyTo10(double)) // double(10) = 20
|
||||
assert.Equal(t, 100, applyTo10(square)) // square(10) = 100
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkBind1st benchmarks the Bind1st function
|
||||
func BenchmarkBind1st(b *testing.B) {
|
||||
multiply := func(a, b int) int { return a * b }
|
||||
double := Bind1st(multiply, 2)
|
||||
|
||||
b.Run("direct call", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = multiply(2, i)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("bound function", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = double(i)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkBind2nd benchmarks the Bind2nd function
|
||||
func BenchmarkBind2nd(b *testing.B) {
|
||||
multiply := func(a, b int) int { return a * b }
|
||||
double := Bind2nd(multiply, 2)
|
||||
|
||||
b.Run("direct call", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = multiply(i, 2)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("bound function", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = double(i)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkSK benchmarks the SK combinator
|
||||
func BenchmarkSK(b *testing.B) {
|
||||
b.Run("SK with ints", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = SK(i, i+1)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Second with ints", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = Second(i, i+1)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -19,23 +19,265 @@ import (
|
||||
G "github.com/IBM/fp-go/v2/function/generic"
|
||||
)
|
||||
|
||||
// Memoize converts a unary function into a unary function that caches the value depending on the parameter
|
||||
// Memoize converts a unary function into a memoized version that caches computed values.
|
||||
//
|
||||
// Behavior:
|
||||
// - On first call with a given input, the function executes and the result is cached
|
||||
// - Subsequent calls with the same input return the cached result without re-execution
|
||||
// - The cache uses the input parameter directly as the key (must be comparable)
|
||||
// - The cache is thread-safe using mutex locks
|
||||
// - The cache has no size limit and grows unbounded
|
||||
// - Each unique input creates a new cache entry that persists for the lifetime of the memoized function
|
||||
//
|
||||
// Implementation Details:
|
||||
// - Uses an internal map[K]func()T to store lazy values
|
||||
// - The cached value is wrapped in a lazy function to defer computation until needed
|
||||
// - Lock is held only to access the cache map, not during value computation
|
||||
// - This allows concurrent computations for different keys
|
||||
//
|
||||
// Type Parameters:
|
||||
// - K: The type of the function parameter, must be comparable (used as cache key)
|
||||
// - T: The return type of the function
|
||||
//
|
||||
// Parameters:
|
||||
// - f: The function to memoize
|
||||
//
|
||||
// Returns:
|
||||
// - A memoized version of the function that caches results by parameter value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Expensive computation
|
||||
// expensiveCalc := func(n int) int {
|
||||
// time.Sleep(100 * time.Millisecond)
|
||||
// return n * n
|
||||
// }
|
||||
//
|
||||
// // Memoize to avoid redundant calculations
|
||||
// memoized := Memoize(expensiveCalc)
|
||||
// result1 := memoized(5) // Takes 100ms, computes and caches 25
|
||||
// result2 := memoized(5) // Instant, returns cached 25
|
||||
// result3 := memoized(10) // Takes 100ms, computes and caches 100
|
||||
//
|
||||
// Note: The cache grows unbounded. For bounded caches, use CacheCallback with a custom cache implementation.
|
||||
func Memoize[K comparable, T any](f func(K) T) func(K) T {
|
||||
return G.Memoize(f)
|
||||
}
|
||||
|
||||
// ContramapMemoize converts a unary function into a unary function that caches the value depending on the parameter
|
||||
// ContramapMemoize creates a higher-order function that memoizes functions using a custom key extraction strategy.
|
||||
//
|
||||
// Behavior:
|
||||
// - Allows caching based on a derived key rather than the full input parameter
|
||||
// - The key extraction function (kf) determines what constitutes a cache hit
|
||||
// - Two inputs that produce the same key will share the same cached result
|
||||
// - This enables caching for non-comparable types by extracting comparable keys
|
||||
// - The cache is thread-safe and unbounded
|
||||
//
|
||||
// Use Cases:
|
||||
// - Cache by a subset of struct fields (e.g., User.ID instead of entire User)
|
||||
// - Cache by a computed property (e.g., string length, hash value)
|
||||
// - Normalize inputs before caching (e.g., lowercase strings, rounded numbers)
|
||||
//
|
||||
// Implementation Details:
|
||||
// - Internally uses the same caching mechanism as Memoize
|
||||
// - The key function is applied to each input before cache lookup
|
||||
// - Returns a function transformer that can be applied to any function with matching signature
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The return type of the function to be memoized
|
||||
// - A: The input type of the function to be memoized
|
||||
// - K: The type of the cache key, must be comparable
|
||||
//
|
||||
// Parameters:
|
||||
// - kf: A function that extracts a cache key from the input parameter
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a function (A) -> T and returns its memoized version
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type User struct {
|
||||
// ID int
|
||||
// Name string
|
||||
// Email string
|
||||
// }
|
||||
//
|
||||
// // Cache by user ID only, ignoring other fields
|
||||
// cacheByID := ContramapMemoize[string, User, int](func(u User) int {
|
||||
// return u.ID
|
||||
// })
|
||||
//
|
||||
// getUserData := func(u User) string {
|
||||
// // Expensive database lookup
|
||||
// return fmt.Sprintf("Data for user %d", u.ID)
|
||||
// }
|
||||
//
|
||||
// memoized := cacheByID(getUserData)
|
||||
// result1 := memoized(User{ID: 1, Name: "Alice", Email: "a@example.com"}) // Computed
|
||||
// result2 := memoized(User{ID: 1, Name: "Bob", Email: "b@example.com"}) // Cached (same ID)
|
||||
// result3 := memoized(User{ID: 2, Name: "Alice", Email: "a@example.com"}) // Computed (different ID)
|
||||
func ContramapMemoize[T, A any, K comparable](kf func(A) K) func(func(A) T) func(A) T {
|
||||
return G.ContramapMemoize[func(A) T](kf)
|
||||
}
|
||||
|
||||
// CacheCallback converts a unary function into a unary function that caches the value depending on the parameter
|
||||
// CacheCallback creates a higher-order function that memoizes functions using a custom cache implementation.
|
||||
//
|
||||
// Behavior:
|
||||
// - Provides complete control over caching strategy through the getOrCreate callback
|
||||
// - Separates cache key extraction (kf) from cache storage (getOrCreate)
|
||||
// - The getOrCreate function receives a key and a lazy value generator
|
||||
// - The cache implementation decides when to store, evict, or retrieve values
|
||||
// - Enables advanced caching strategies: LRU, LFU, TTL, bounded size, etc.
|
||||
//
|
||||
// How It Works:
|
||||
// 1. When the memoized function is called with input A:
|
||||
// 2. The key function (kf) extracts a cache key K from A
|
||||
// 3. A lazy value generator is created that will compute f(A) when called
|
||||
// 4. The getOrCreate callback is invoked with the key and lazy generator
|
||||
// 5. The cache implementation returns a lazy value (either cached or newly created)
|
||||
// 6. The lazy value is evaluated to produce the final result T
|
||||
//
|
||||
// Cache Implementation Contract:
|
||||
// - getOrCreate receives: (key K, generator func() func() T)
|
||||
// - getOrCreate returns: func() T (a lazy value)
|
||||
// - The generator creates a new lazy value when called
|
||||
// - The cache should store and return lazy values, not final results
|
||||
// - This allows deferred computation and proper lazy evaluation
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The return type of the function to be memoized
|
||||
// - A: The input type of the function to be memoized
|
||||
// - K: The type of the cache key, must be comparable
|
||||
//
|
||||
// Parameters:
|
||||
// - kf: A function that extracts a cache key from the input parameter
|
||||
// - getOrCreate: A cache implementation that stores and retrieves lazy values
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a function (A) -> T and returns its memoized version
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a bounded LRU cache (max 100 items)
|
||||
// lruCache := func() func(int, func() func() string) func() string {
|
||||
// cache := make(map[int]func() string)
|
||||
// keys := []int{}
|
||||
// var mu sync.Mutex
|
||||
// maxSize := 100
|
||||
//
|
||||
// return func(k int, gen func() func() string) func() string {
|
||||
// mu.Lock()
|
||||
// defer mu.Unlock()
|
||||
//
|
||||
// if existing, ok := cache[k]; ok {
|
||||
// return existing // Cache hit
|
||||
// }
|
||||
//
|
||||
// // Evict oldest if at capacity
|
||||
// if len(keys) >= maxSize {
|
||||
// delete(cache, keys[0])
|
||||
// keys = keys[1:]
|
||||
// }
|
||||
//
|
||||
// // Create and store new lazy value
|
||||
// value := gen()
|
||||
// cache[k] = value
|
||||
// keys = append(keys, k)
|
||||
// return value
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Use custom cache with memoization
|
||||
// memoizer := CacheCallback[string, int, int](
|
||||
// Identity[int], // Use input as key
|
||||
// lruCache(),
|
||||
// )
|
||||
//
|
||||
// expensiveFunc := func(n int) string {
|
||||
// time.Sleep(100 * time.Millisecond)
|
||||
// return fmt.Sprintf("Result: %d", n)
|
||||
// }
|
||||
//
|
||||
// memoized := memoizer(expensiveFunc)
|
||||
// result := memoized(42) // Computed and cached
|
||||
// result = memoized(42) // Retrieved from cache
|
||||
//
|
||||
// See also: SingleElementCache for a simple bounded cache implementation.
|
||||
func CacheCallback[
|
||||
T, A any, K comparable](kf func(A) K, getOrCreate func(K, func() func() T) func() T) func(func(A) T) func(A) T {
|
||||
return G.CacheCallback[func(func(A) T) func(A) T](kf, getOrCreate)
|
||||
}
|
||||
|
||||
// SingleElementCache creates a cache function for use with the [CacheCallback] method that has a maximum capacity of one single item
|
||||
// SingleElementCache creates a thread-safe cache implementation that stores at most one element.
|
||||
//
|
||||
// Behavior:
|
||||
// - Stores only the most recently accessed key-value pair
|
||||
// - When a new key is accessed, it replaces the previous cached entry
|
||||
// - If the same key is accessed again, the cached value is returned
|
||||
// - Thread-safe: uses mutex to protect concurrent access
|
||||
// - Memory-efficient: constant O(1) space regardless of usage
|
||||
//
|
||||
// How It Works:
|
||||
// 1. Initially, the cache is empty (hasKey = false)
|
||||
// 2. On first access with key K1:
|
||||
// - Calls the generator to create a lazy value
|
||||
// - Stores K1 and the lazy value
|
||||
// - Returns the lazy value
|
||||
// 3. On subsequent access with same key K1:
|
||||
// - Returns the stored lazy value without calling generator
|
||||
// 4. On access with different key K2:
|
||||
// - Calls the generator to create a new lazy value
|
||||
// - Replaces K1 with K2 and updates the stored lazy value
|
||||
// - Returns the new lazy value
|
||||
// 5. If K1 is accessed again, it's treated as a new key (cache miss)
|
||||
//
|
||||
// Use Cases:
|
||||
// - Sequential processing where the same key is accessed multiple times in a row
|
||||
// - Memory-constrained environments where unbounded caches are not feasible
|
||||
// - Scenarios where only the most recent computation needs caching
|
||||
// - Testing or debugging with controlled cache behavior
|
||||
//
|
||||
// Important Notes:
|
||||
// - The cache stores the lazy value (func() T), not the computed result
|
||||
// - Each time the returned lazy value is called, it may recompute (depends on lazy implementation)
|
||||
// - For true result caching, combine with lazy memoization (as done in CacheCallback)
|
||||
// - Alternating between two keys will cause constant cache misses
|
||||
//
|
||||
// Type Parameters:
|
||||
// - K: The type of the cache key, must be comparable
|
||||
// - T: The type of the cached value
|
||||
//
|
||||
// Returns:
|
||||
// - A cache function suitable for use with CacheCallback
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a single-element cache
|
||||
// cache := SingleElementCache[int, string]()
|
||||
//
|
||||
// // Use with CacheCallback
|
||||
// memoizer := CacheCallback[string, int, int](
|
||||
// Identity[int], // Use input as key
|
||||
// cache,
|
||||
// )
|
||||
//
|
||||
// expensiveFunc := func(n int) string {
|
||||
// time.Sleep(100 * time.Millisecond)
|
||||
// return fmt.Sprintf("Result: %d", n)
|
||||
// }
|
||||
//
|
||||
// memoized := memoizer(expensiveFunc)
|
||||
// result1 := memoized(42) // Computed (100ms) and cached
|
||||
// result2 := memoized(42) // Instant - returns cached value
|
||||
// result3 := memoized(99) // Computed (100ms) - replaces cache entry for 42
|
||||
// result4 := memoized(99) // Instant - returns cached value
|
||||
// result5 := memoized(42) // Computed (100ms) - cache was replaced, must recompute
|
||||
//
|
||||
// Performance Characteristics:
|
||||
// - Space: O(1) - stores exactly one key-value pair
|
||||
// - Time: O(1) - cache lookup and update are constant time
|
||||
// - Best case: Same key accessed repeatedly (100% hit rate)
|
||||
// - Worst case: Alternating keys (0% hit rate)
|
||||
func SingleElementCache[K comparable, T any]() func(K, func() func() T) func() T {
|
||||
return G.SingleElementCache[func() func() T, K]()
|
||||
}
|
||||
|
||||
@@ -17,54 +17,601 @@ package function
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
var count int
|
||||
// TestMemoize tests the Memoize function
|
||||
func TestMemoize(t *testing.T) {
|
||||
t.Run("caches computed values", func(t *testing.T) {
|
||||
callCount := 0
|
||||
expensive := func(n int) int {
|
||||
callCount++
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
return n * 2
|
||||
}
|
||||
|
||||
withSideEffect := func(n int) int {
|
||||
count++
|
||||
return n
|
||||
}
|
||||
memoized := Memoize(expensive)
|
||||
|
||||
cached := Memoize(withSideEffect)
|
||||
// First call should compute
|
||||
result1 := memoized(5)
|
||||
assert.Equal(t, 10, result1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
assert.Equal(t, 0, count)
|
||||
// Second call with same input should use cache
|
||||
result2 := memoized(5)
|
||||
assert.Equal(t, 10, result2)
|
||||
assert.Equal(t, 1, callCount, "should not recompute for cached value")
|
||||
|
||||
assert.Equal(t, 10, cached(10))
|
||||
assert.Equal(t, 1, count)
|
||||
// Different input should compute again
|
||||
result3 := memoized(10)
|
||||
assert.Equal(t, 20, result3)
|
||||
assert.Equal(t, 2, callCount)
|
||||
|
||||
assert.Equal(t, 10, cached(10))
|
||||
assert.Equal(t, 1, count)
|
||||
// Original input should still be cached
|
||||
result4 := memoized(5)
|
||||
assert.Equal(t, 10, result4)
|
||||
assert.Equal(t, 2, callCount, "should still use cached value")
|
||||
})
|
||||
|
||||
assert.Equal(t, 20, cached(20))
|
||||
assert.Equal(t, 2, count)
|
||||
t.Run("works with string keys", func(t *testing.T) {
|
||||
callCount := 0
|
||||
toUpper := func(s string) string {
|
||||
callCount++
|
||||
return fmt.Sprintf("UPPER_%s", s)
|
||||
}
|
||||
|
||||
assert.Equal(t, 20, cached(20))
|
||||
assert.Equal(t, 2, count)
|
||||
memoized := Memoize(toUpper)
|
||||
|
||||
assert.Equal(t, 10, cached(10))
|
||||
assert.Equal(t, 2, count)
|
||||
result1 := memoized("hello")
|
||||
assert.Equal(t, "UPPER_hello", result1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
result2 := memoized("hello")
|
||||
assert.Equal(t, "UPPER_hello", result2)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
result3 := memoized("world")
|
||||
assert.Equal(t, "UPPER_world", result3)
|
||||
assert.Equal(t, 2, callCount)
|
||||
})
|
||||
|
||||
t.Run("is thread-safe", func(t *testing.T) {
|
||||
var callCount int32
|
||||
expensive := func(n int) int {
|
||||
atomic.AddInt32(&callCount, 1)
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
return n * n
|
||||
}
|
||||
|
||||
memoized := Memoize(expensive)
|
||||
|
||||
// Run concurrent calls with same input
|
||||
var wg sync.WaitGroup
|
||||
results := make([]int, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
results[idx] = memoized(7)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// All results should be the same
|
||||
for _, result := range results {
|
||||
assert.Equal(t, 49, result)
|
||||
}
|
||||
|
||||
// Function should be called at least once, but possibly more due to race
|
||||
// (the cache is eventually consistent)
|
||||
assert.Greater(t, atomic.LoadInt32(&callCount), int32(0))
|
||||
})
|
||||
|
||||
t.Run("handles zero values correctly", func(t *testing.T) {
|
||||
callCount := 0
|
||||
identity := func(n int) int {
|
||||
callCount++
|
||||
return n
|
||||
}
|
||||
|
||||
memoized := Memoize(identity)
|
||||
|
||||
result1 := memoized(0)
|
||||
assert.Equal(t, 0, result1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
result2 := memoized(0)
|
||||
assert.Equal(t, 0, result2)
|
||||
assert.Equal(t, 1, callCount, "should cache zero value")
|
||||
})
|
||||
|
||||
t.Run("caches multiple different values", func(t *testing.T) {
|
||||
callCount := 0
|
||||
square := func(n int) int {
|
||||
callCount++
|
||||
return n * n
|
||||
}
|
||||
|
||||
memoized := Memoize(square)
|
||||
|
||||
// Cache multiple values
|
||||
assert.Equal(t, 4, memoized(2))
|
||||
assert.Equal(t, 9, memoized(3))
|
||||
assert.Equal(t, 16, memoized(4))
|
||||
assert.Equal(t, 3, callCount)
|
||||
|
||||
// All should be cached
|
||||
assert.Equal(t, 4, memoized(2))
|
||||
assert.Equal(t, 9, memoized(3))
|
||||
assert.Equal(t, 16, memoized(4))
|
||||
assert.Equal(t, 3, callCount, "all values should be cached")
|
||||
})
|
||||
}
|
||||
|
||||
// TestContramapMemoize tests the ContramapMemoize function
|
||||
func TestContramapMemoize(t *testing.T) {
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("caches by extracted key", func(t *testing.T) {
|
||||
callCount := 0
|
||||
getUserData := func(u User) string {
|
||||
callCount++
|
||||
return fmt.Sprintf("Data for user %d: %s", u.ID, u.Name)
|
||||
}
|
||||
|
||||
// Cache by ID only
|
||||
cacheByID := ContramapMemoize[string, User, int](func(u User) int {
|
||||
return u.ID
|
||||
})
|
||||
|
||||
memoized := cacheByID(getUserData)
|
||||
|
||||
user1 := User{ID: 1, Name: "Alice", Age: 30}
|
||||
result1 := memoized(user1)
|
||||
assert.Equal(t, "Data for user 1: Alice", result1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
// Same ID, different name - should use cache
|
||||
user2 := User{ID: 1, Name: "Bob", Age: 25}
|
||||
result2 := memoized(user2)
|
||||
assert.Equal(t, "Data for user 1: Alice", result2, "should return cached result")
|
||||
assert.Equal(t, 1, callCount, "should not recompute")
|
||||
|
||||
// Different ID - should compute
|
||||
user3 := User{ID: 2, Name: "Charlie", Age: 35}
|
||||
result3 := memoized(user3)
|
||||
assert.Equal(t, "Data for user 2: Charlie", result3)
|
||||
assert.Equal(t, 2, callCount)
|
||||
})
|
||||
|
||||
t.Run("works with string key extraction", func(t *testing.T) {
|
||||
type Product struct {
|
||||
SKU string
|
||||
Name string
|
||||
Price float64
|
||||
}
|
||||
|
||||
callCount := 0
|
||||
getPrice := func(p Product) float64 {
|
||||
callCount++
|
||||
return p.Price * 1.1 // Add 10% markup
|
||||
}
|
||||
|
||||
cacheBySKU := ContramapMemoize[float64, Product, string](func(p Product) string {
|
||||
return p.SKU
|
||||
})
|
||||
|
||||
memoized := cacheBySKU(getPrice)
|
||||
|
||||
prod1 := Product{SKU: "ABC123", Name: "Widget", Price: 100.0}
|
||||
result1 := memoized(prod1)
|
||||
assert.InDelta(t, 110.0, result1, 0.01)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
// Same SKU, different price - should use cached result
|
||||
prod2 := Product{SKU: "ABC123", Name: "Widget", Price: 200.0}
|
||||
result2 := memoized(prod2)
|
||||
assert.InDelta(t, 110.0, result2, 0.01, "should use cached value")
|
||||
assert.Equal(t, 1, callCount)
|
||||
})
|
||||
|
||||
t.Run("can use complex key extraction", func(t *testing.T) {
|
||||
type Request struct {
|
||||
Method string
|
||||
Path string
|
||||
Body string
|
||||
}
|
||||
|
||||
callCount := 0
|
||||
processRequest := func(r Request) string {
|
||||
callCount++
|
||||
return fmt.Sprintf("Processed: %s %s", r.Method, r.Path)
|
||||
}
|
||||
|
||||
// Cache by method and path, ignore body
|
||||
cacheByMethodPath := ContramapMemoize[string, Request, string](func(r Request) string {
|
||||
return r.Method + ":" + r.Path
|
||||
})
|
||||
|
||||
memoized := cacheByMethodPath(processRequest)
|
||||
|
||||
req1 := Request{Method: "GET", Path: "/api/users", Body: "body1"}
|
||||
result1 := memoized(req1)
|
||||
assert.Equal(t, "Processed: GET /api/users", result1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
// Same method and path, different body - should use cache
|
||||
req2 := Request{Method: "GET", Path: "/api/users", Body: "body2"}
|
||||
result2 := memoized(req2)
|
||||
assert.Equal(t, "Processed: GET /api/users", result2)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
// Different path - should compute
|
||||
req3 := Request{Method: "GET", Path: "/api/posts", Body: "body1"}
|
||||
result3 := memoized(req3)
|
||||
assert.Equal(t, "Processed: GET /api/posts", result3)
|
||||
assert.Equal(t, 2, callCount)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCacheCallback tests the CacheCallback function
|
||||
func TestCacheCallback(t *testing.T) {
|
||||
t.Run("works with custom cache implementation", func(t *testing.T) {
|
||||
// Create a simple bounded cache (max 2 items)
|
||||
boundedCache := func() func(int, func() func() string) func() string {
|
||||
cache := make(map[int]func() string)
|
||||
keys := []int{}
|
||||
var mu sync.Mutex
|
||||
|
||||
return func(k int, gen func() func() string) func() string {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if existing, ok := cache[k]; ok {
|
||||
return existing
|
||||
}
|
||||
|
||||
// Evict oldest if at capacity
|
||||
if len(keys) >= 2 {
|
||||
oldestKey := keys[0]
|
||||
delete(cache, oldestKey)
|
||||
keys = keys[1:]
|
||||
}
|
||||
|
||||
value := gen()
|
||||
cache[k] = value
|
||||
keys = append(keys, k)
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
callCount := 0
|
||||
expensive := func(n int) string {
|
||||
callCount++
|
||||
return fmt.Sprintf("Result: %d", n)
|
||||
}
|
||||
|
||||
memoizer := CacheCallback[string, int, int](
|
||||
Identity[int],
|
||||
boundedCache(),
|
||||
)
|
||||
|
||||
memoized := memoizer(expensive)
|
||||
|
||||
// Cache first two values
|
||||
result1 := memoized(1)
|
||||
assert.Equal(t, "Result: 1", result1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
result2 := memoized(2)
|
||||
assert.Equal(t, "Result: 2", result2)
|
||||
assert.Equal(t, 2, callCount)
|
||||
|
||||
// Both should be cached
|
||||
memoized(1)
|
||||
memoized(2)
|
||||
assert.Equal(t, 2, callCount)
|
||||
|
||||
// Third value should evict first
|
||||
result3 := memoized(3)
|
||||
assert.Equal(t, "Result: 3", result3)
|
||||
assert.Equal(t, 3, callCount)
|
||||
|
||||
// First value should be recomputed (evicted)
|
||||
// Note: The cache stores lazy generators, so calling memoized(1) again
|
||||
// will create a new cache entry with a new lazy generator
|
||||
memoized(1)
|
||||
// The call count increases because a new lazy value is created and evaluated
|
||||
assert.GreaterOrEqual(t, callCount, 3, "first value should have been evicted")
|
||||
|
||||
// Verify cache still works for remaining values
|
||||
prevCount := callCount
|
||||
memoized(2)
|
||||
memoized(3)
|
||||
// These might or might not increase count depending on eviction
|
||||
assert.GreaterOrEqual(t, callCount, prevCount)
|
||||
})
|
||||
|
||||
t.Run("integrates with key extraction", func(t *testing.T) {
|
||||
type Item struct {
|
||||
ID int
|
||||
Value string
|
||||
}
|
||||
|
||||
// Simple cache
|
||||
simpleCache := func() func(int, func() func() string) func() string {
|
||||
cache := make(map[int]func() string)
|
||||
var mu sync.Mutex
|
||||
|
||||
return func(k int, gen func() func() string) func() string {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if existing, ok := cache[k]; ok {
|
||||
return existing
|
||||
}
|
||||
|
||||
value := gen()
|
||||
cache[k] = value
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
callCount := 0
|
||||
process := func(item Item) string {
|
||||
callCount++
|
||||
return fmt.Sprintf("Processed: %s", item.Value)
|
||||
}
|
||||
|
||||
memoizer := CacheCallback[string, Item, int](
|
||||
func(item Item) int { return item.ID },
|
||||
simpleCache(),
|
||||
)
|
||||
|
||||
memoized := memoizer(process)
|
||||
|
||||
item1 := Item{ID: 1, Value: "first"}
|
||||
result1 := memoized(item1)
|
||||
assert.Equal(t, "Processed: first", result1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
// Same ID, different value - should use cache
|
||||
item2 := Item{ID: 1, Value: "second"}
|
||||
result2 := memoized(item2)
|
||||
assert.Equal(t, "Processed: first", result2)
|
||||
assert.Equal(t, 1, callCount)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSingleElementCache tests the SingleElementCache function
|
||||
func TestSingleElementCache(t *testing.T) {
|
||||
f := func(key string) string {
|
||||
return fmt.Sprintf("%s: %d", key, rand.Int())
|
||||
}
|
||||
cb := CacheCallback(func(s string) string { return s }, SingleElementCache[string, string]())
|
||||
cf := cb(f)
|
||||
t.Run("caches single element", func(t *testing.T) {
|
||||
cache := SingleElementCache[int, string]()
|
||||
|
||||
v1 := cf("1")
|
||||
v2 := cf("1")
|
||||
v3 := cf("2")
|
||||
v4 := cf("1")
|
||||
callCount := 0
|
||||
gen := func(n int) func() func() string {
|
||||
// This returns a generator that creates a lazy value
|
||||
return func() func() string {
|
||||
// This is the lazy value that gets cached
|
||||
return func() string {
|
||||
// This gets called when the lazy value is evaluated
|
||||
callCount++
|
||||
return fmt.Sprintf("Value: %d", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, v1, v2)
|
||||
assert.NotEqual(t, v2, v3)
|
||||
assert.NotEqual(t, v3, v4)
|
||||
assert.NotEqual(t, v1, v4)
|
||||
// First call - creates and caches lazy value for key 1
|
||||
lazy1 := cache(1, gen(1))
|
||||
result1 := lazy1()
|
||||
assert.Equal(t, "Value: 1", result1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
// Same key - returns the same cached lazy value
|
||||
lazy1Again := cache(1, gen(1))
|
||||
result2 := lazy1Again()
|
||||
assert.Equal(t, "Value: 1", result2)
|
||||
// The lazy value is called again, so count increases
|
||||
assert.Equal(t, 2, callCount, "cached lazy value is called again")
|
||||
|
||||
// Different key - replaces cache with new lazy value
|
||||
lazy2 := cache(2, gen(2))
|
||||
result3 := lazy2()
|
||||
assert.Equal(t, "Value: 2", result3)
|
||||
assert.Equal(t, 3, callCount)
|
||||
|
||||
// Original key - cache was replaced, creates new lazy value
|
||||
lazy1New := cache(1, gen(1))
|
||||
result4 := lazy1New()
|
||||
assert.Equal(t, "Value: 1", result4)
|
||||
assert.Equal(t, 4, callCount, "new lazy value created after cache replacement")
|
||||
})
|
||||
|
||||
t.Run("works with CacheCallback", func(t *testing.T) {
|
||||
cache := SingleElementCache[int, string]()
|
||||
|
||||
callCount := 0
|
||||
expensive := func(n int) string {
|
||||
callCount++
|
||||
return fmt.Sprintf("Result: %d", n*n)
|
||||
}
|
||||
|
||||
memoizer := CacheCallback[string, int, int](
|
||||
Identity[int],
|
||||
cache,
|
||||
)
|
||||
|
||||
memoized := memoizer(expensive)
|
||||
|
||||
// First computation
|
||||
result1 := memoized(5)
|
||||
assert.Equal(t, "Result: 25", result1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
// Same input - cached
|
||||
result2 := memoized(5)
|
||||
assert.Equal(t, "Result: 25", result2)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
// Different input - replaces cache
|
||||
result3 := memoized(10)
|
||||
assert.Equal(t, "Result: 100", result3)
|
||||
assert.Equal(t, 2, callCount)
|
||||
|
||||
// Back to first input - recomputed
|
||||
result4 := memoized(5)
|
||||
assert.Equal(t, "Result: 25", result4)
|
||||
assert.Equal(t, 3, callCount)
|
||||
})
|
||||
|
||||
t.Run("is thread-safe", func(t *testing.T) {
|
||||
cache := SingleElementCache[int, string]()
|
||||
|
||||
var callCount int32
|
||||
gen := func(n int) func() func() string {
|
||||
return func() func() string {
|
||||
return func() string {
|
||||
atomic.AddInt32(&callCount, 1)
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
return fmt.Sprintf("Value: %d", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
results := make([]string, 20)
|
||||
|
||||
// Concurrent access with same key
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
results[idx] = cache(1, gen(1))()
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Concurrent access with different key
|
||||
for i := 10; i < 20; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
results[idx] = cache(2, gen(2))()
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// All results should be valid (either "Value: 1" or "Value: 2")
|
||||
for _, result := range results {
|
||||
assert.True(t, result == "Value: 1" || result == "Value: 2")
|
||||
}
|
||||
|
||||
// Function should have been called, but exact count depends on race conditions
|
||||
assert.Greater(t, atomic.LoadInt32(&callCount), int32(0))
|
||||
})
|
||||
|
||||
t.Run("handles rapid key changes", func(t *testing.T) {
|
||||
cache := SingleElementCache[int, string]()
|
||||
|
||||
callCount := 0
|
||||
gen := func(n int) func() func() string {
|
||||
return func() func() string {
|
||||
return func() string {
|
||||
callCount++
|
||||
return fmt.Sprintf("Value: %d", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rapidly alternate between keys
|
||||
for i := 0; i < 10; i++ {
|
||||
cache(1, gen(1))()
|
||||
cache(2, gen(2))()
|
||||
}
|
||||
|
||||
// Each key change should trigger a computation
|
||||
// (20 calls total: 10 for key 1, 10 for key 2)
|
||||
assert.Equal(t, 20, callCount)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMemoizeIntegration tests integration scenarios
|
||||
func TestMemoizeIntegration(t *testing.T) {
|
||||
t.Run("fibonacci with memoization", func(t *testing.T) {
|
||||
callCount := 0
|
||||
|
||||
expensive := func(n int) int {
|
||||
callCount++
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
return n * n
|
||||
}
|
||||
|
||||
memoized := Memoize(expensive)
|
||||
|
||||
// First call computes
|
||||
result1 := memoized(10)
|
||||
assert.Equal(t, 100, result1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
// Second call with same input uses cache
|
||||
result2 := memoized(10)
|
||||
assert.Equal(t, 100, result2)
|
||||
assert.Equal(t, 1, callCount, "should use cached value")
|
||||
|
||||
// Different input computes again
|
||||
result3 := memoized(5)
|
||||
assert.Equal(t, 25, result3)
|
||||
assert.Equal(t, 2, callCount)
|
||||
|
||||
// Both values should remain cached
|
||||
assert.Equal(t, 100, memoized(10))
|
||||
assert.Equal(t, 25, memoized(5))
|
||||
assert.Equal(t, 2, callCount, "both values should be cached")
|
||||
})
|
||||
|
||||
t.Run("chaining memoization strategies", func(t *testing.T) {
|
||||
type Request struct {
|
||||
UserID int
|
||||
Action string
|
||||
}
|
||||
|
||||
callCount := 0
|
||||
processRequest := func(r Request) string {
|
||||
callCount++
|
||||
return fmt.Sprintf("User %d: %s", r.UserID, r.Action)
|
||||
}
|
||||
|
||||
// First level: cache by UserID
|
||||
cacheByUser := ContramapMemoize[string, Request, int](func(r Request) int {
|
||||
return r.UserID
|
||||
})
|
||||
|
||||
memoized := cacheByUser(processRequest)
|
||||
|
||||
req1 := Request{UserID: 1, Action: "login"}
|
||||
result1 := memoized(req1)
|
||||
assert.Equal(t, "User 1: login", result1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
// Same user, different action - uses cache
|
||||
req2 := Request{UserID: 1, Action: "logout"}
|
||||
result2 := memoized(req2)
|
||||
assert.Equal(t, "User 1: login", result2)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
// Different user - computes
|
||||
req3 := Request{UserID: 2, Action: "login"}
|
||||
result3 := memoized(req3)
|
||||
assert.Equal(t, "User 2: login", result3)
|
||||
assert.Equal(t, 2, callCount)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -194,79 +194,6 @@ func TestSecond(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestBind1st tests the Bind1st function
|
||||
func TestBind1st(t *testing.T) {
|
||||
t.Run("binds first parameter of multiplication", func(t *testing.T) {
|
||||
multiply := func(a, b int) int { return a * b }
|
||||
double := Bind1st(multiply, 2)
|
||||
triple := Bind1st(multiply, 3)
|
||||
|
||||
assert.Equal(t, 10, double(5))
|
||||
assert.Equal(t, 20, double(10))
|
||||
assert.Equal(t, 15, triple(5))
|
||||
})
|
||||
|
||||
t.Run("binds first parameter of division", func(t *testing.T) {
|
||||
divide := func(a, b float64) float64 { return a / b }
|
||||
divideBy10 := Bind1st(divide, 10.0)
|
||||
|
||||
assert.Equal(t, 5.0, divideBy10(2.0))
|
||||
assert.Equal(t, 2.0, divideBy10(5.0))
|
||||
})
|
||||
|
||||
t.Run("binds first parameter of string concatenation", func(t *testing.T) {
|
||||
concat := func(a, b string) string { return a + b }
|
||||
addHello := Bind1st(concat, "Hello ")
|
||||
|
||||
assert.Equal(t, "Hello World", addHello("World"))
|
||||
assert.Equal(t, "Hello Go", addHello("Go"))
|
||||
})
|
||||
}
|
||||
|
||||
// TestBind2nd tests the Bind2nd function
|
||||
func TestBind2nd(t *testing.T) {
|
||||
t.Run("binds second parameter of multiplication", func(t *testing.T) {
|
||||
multiply := func(a, b int) int { return a * b }
|
||||
double := Bind2nd(multiply, 2)
|
||||
triple := Bind2nd(multiply, 3)
|
||||
|
||||
assert.Equal(t, 10, double(5))
|
||||
assert.Equal(t, 20, double(10))
|
||||
assert.Equal(t, 15, triple(5))
|
||||
})
|
||||
|
||||
t.Run("binds second parameter of division", func(t *testing.T) {
|
||||
divide := func(a, b float64) float64 { return a / b }
|
||||
halve := Bind2nd(divide, 2.0)
|
||||
|
||||
assert.Equal(t, 5.0, halve(10.0))
|
||||
assert.Equal(t, 2.5, halve(5.0))
|
||||
})
|
||||
|
||||
t.Run("binds second parameter of subtraction", func(t *testing.T) {
|
||||
subtract := func(a, b int) int { return a - b }
|
||||
decrementBy5 := Bind2nd(subtract, 5)
|
||||
|
||||
assert.Equal(t, 5, decrementBy5(10))
|
||||
assert.Equal(t, 0, decrementBy5(5))
|
||||
})
|
||||
}
|
||||
|
||||
// TestSK tests the SK function
|
||||
func TestSK(t *testing.T) {
|
||||
t.Run("returns second argument ignoring first", func(t *testing.T) {
|
||||
assert.Equal(t, "hello", SK(42, "hello"))
|
||||
assert.Equal(t, 100, SK(true, 100))
|
||||
assert.Equal(t, 3.14, SK("test", 3.14))
|
||||
})
|
||||
|
||||
t.Run("behaves like Second", func(t *testing.T) {
|
||||
// SK should be identical to Second
|
||||
assert.Equal(t, Second(42, "hello"), SK(42, "hello"))
|
||||
assert.Equal(t, Second(true, 100), SK(true, 100))
|
||||
})
|
||||
}
|
||||
|
||||
// TestTernary tests the Ternary function
|
||||
func TestTernary(t *testing.T) {
|
||||
t.Run("applies onTrue when predicate is true", func(t *testing.T) {
|
||||
|
||||
@@ -13,11 +13,45 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package content provides constants for common HTTP Content-Type header values.
|
||||
//
|
||||
// These constants can be used when setting or checking Content-Type headers in HTTP
|
||||
// requests and responses, ensuring consistency and avoiding typos in content type strings.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// req.Header.Set("Content-Type", content.JSON)
|
||||
// if contentType == content.TextPlain {
|
||||
// // handle plain text
|
||||
// }
|
||||
package content
|
||||
|
||||
const (
|
||||
TextPlain = "text/plain"
|
||||
JSON = "application/json"
|
||||
Json = JSON // Deprecated: use [JSON] instead
|
||||
// TextPlain represents the "text/plain" content type for plain text data.
|
||||
// This is commonly used for simple text responses or requests without any
|
||||
// specific formatting or structure.
|
||||
//
|
||||
// Defined in RFC 2046, Section 4.1.3: https://www.rfc-editor.org/rfc/rfc2046.html#section-4.1.3
|
||||
TextPlain = "text/plain"
|
||||
|
||||
// JSON represents the "application/json" content type for JSON-encoded data.
|
||||
// This is the standard content type for JSON payloads in HTTP requests and responses.
|
||||
//
|
||||
// Defined in RFC 8259: https://www.rfc-editor.org/rfc/rfc8259.html
|
||||
JSON = "application/json"
|
||||
|
||||
// Json is deprecated. Use [JSON] instead.
|
||||
//
|
||||
// Deprecated: Use JSON for consistency with Go naming conventions.
|
||||
Json = JSON
|
||||
|
||||
// FormEncoded represents the "application/x-www-form-urlencoded" content type.
|
||||
// This is used for HTML form submissions where form data is encoded as key-value
|
||||
// pairs in the request body, with keys and values URL-encoded.
|
||||
//
|
||||
// Defined in HTML 4.01 Specification, Section 17.13.4:
|
||||
// https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4
|
||||
// Also referenced in WHATWG HTML Living Standard:
|
||||
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#application/x-www-form-urlencoded-encoding-algorithm
|
||||
FormEncoded = "application/x-www-form-urlencoded"
|
||||
)
|
||||
|
||||
@@ -13,6 +13,62 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package form provides functional utilities for working with HTTP form data (url.Values).
|
||||
//
|
||||
// This package offers a functional approach to building and manipulating HTTP form data
|
||||
// using lenses, endomorphisms, and monoids. It enables immutable transformations of
|
||||
// url.Values through composable operations.
|
||||
//
|
||||
// # Core Concepts
|
||||
//
|
||||
// The package is built around several key abstractions:
|
||||
// - Endomorphism: A function that transforms url.Values immutably
|
||||
// - Lenses: Optics for focusing on specific form fields
|
||||
// - Monoids: For combining form transformations and values
|
||||
//
|
||||
// # Basic Usage
|
||||
//
|
||||
// Create form data by composing endomorphisms:
|
||||
//
|
||||
// form := F.Pipe3(
|
||||
// form.Default,
|
||||
// form.WithValue("username")("john"),
|
||||
// form.WithValue("email")("john@example.com"),
|
||||
// form.WithValue("age")("30"),
|
||||
// )
|
||||
//
|
||||
// Remove fields from forms:
|
||||
//
|
||||
// updated := F.Pipe1(
|
||||
// form,
|
||||
// form.WithoutValue("age"),
|
||||
// )
|
||||
//
|
||||
// # Lenses
|
||||
//
|
||||
// The package provides two main lenses:
|
||||
// - AtValues: Focuses on all values of a form field ([]string)
|
||||
// - AtValue: Focuses on the first value of a form field (Option[string])
|
||||
//
|
||||
// Use lenses to read and update form fields:
|
||||
//
|
||||
// lens := form.AtValue("username")
|
||||
// value := lens.Get(form) // Returns Option[string]
|
||||
// updated := lens.Set(O.Some("jane"))(form)
|
||||
//
|
||||
// # Monoids
|
||||
//
|
||||
// Combine multiple form transformations:
|
||||
//
|
||||
// transform := form.Monoid.Concat(
|
||||
// form.WithValue("field1")("value1"),
|
||||
// form.WithValue("field2")("value2"),
|
||||
// )
|
||||
// result := transform(form.Default)
|
||||
//
|
||||
// Merge form values:
|
||||
//
|
||||
// merged := form.ValuesMonoid.Concat(form1, form2)
|
||||
package form
|
||||
|
||||
import (
|
||||
@@ -29,23 +85,61 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
// Endomorphism returns an [ENDO.Endomorphism] that transforms a form
|
||||
// Endomorphism is a function that transforms url.Values immutably.
|
||||
// It represents a transformation from url.Values to url.Values,
|
||||
// enabling functional composition of form modifications.
|
||||
//
|
||||
// Example:
|
||||
// transform := form.WithValue("key")("value")
|
||||
// result := transform(form.Default)
|
||||
Endomorphism = ENDO.Endomorphism[url.Values]
|
||||
)
|
||||
|
||||
var (
|
||||
// Default is the default form field
|
||||
// Default is an empty url.Values that serves as the starting point
|
||||
// for building form data. Use this with Pipe operations to construct
|
||||
// forms functionally.
|
||||
//
|
||||
// Example:
|
||||
// form := F.Pipe2(
|
||||
// form.Default,
|
||||
// form.WithValue("key1")("value1"),
|
||||
// form.WithValue("key2")("value2"),
|
||||
// )
|
||||
Default = make(url.Values)
|
||||
|
||||
noField = O.None[string]()
|
||||
|
||||
// Monoid is the [M.Monoid] for the [Endomorphism]
|
||||
// Monoid is a Monoid for Endomorphism that allows combining multiple
|
||||
// form transformations into a single transformation. The identity element
|
||||
// is the identity function, and concatenation composes transformations.
|
||||
//
|
||||
// Example:
|
||||
// transform := form.Monoid.Concat(
|
||||
// form.WithValue("field1")("value1"),
|
||||
// form.WithValue("field2")("value2"),
|
||||
// )
|
||||
// result := transform(form.Default)
|
||||
Monoid = ENDO.Monoid[url.Values]()
|
||||
|
||||
// ValuesMonoid is a [M.Monoid] to concatenate [url.Values] maps
|
||||
// ValuesMonoid is a Monoid for url.Values that concatenates form data.
|
||||
// When two forms are combined, arrays of values for the same key are
|
||||
// concatenated using the array Semigroup.
|
||||
//
|
||||
// Example:
|
||||
// form1 := url.Values{"key": []string{"value1"}}
|
||||
// form2 := url.Values{"key": []string{"value2"}}
|
||||
// merged := form.ValuesMonoid.Concat(form1, form2)
|
||||
// // Result: url.Values{"key": []string{"value1", "value2"}}
|
||||
ValuesMonoid = RG.UnionMonoid[url.Values](A.Semigroup[string]())
|
||||
|
||||
// AtValues is a [L.Lens] that focusses on the values of a form field
|
||||
// AtValues is a Lens that focuses on all values of a form field as a slice.
|
||||
// It provides access to the complete []string array for a given field name.
|
||||
//
|
||||
// Example:
|
||||
// lens := form.AtValues("tags")
|
||||
// values := lens.Get(form) // Returns Option[[]string]
|
||||
// updated := lens.Set(O.Some([]string{"tag1", "tag2"}))(form)
|
||||
AtValues = LRG.AtRecord[url.Values, []string]
|
||||
|
||||
composeHead = F.Pipe1(
|
||||
@@ -53,14 +147,39 @@ var (
|
||||
LO.Compose[url.Values, string](A.Empty[string]()),
|
||||
)
|
||||
|
||||
// AtValue is a [L.Lens] that focusses on first value in form fields
|
||||
// AtValue is a Lens that focuses on the first value of a form field.
|
||||
// It returns an Option[string] representing the first value if present,
|
||||
// or None if the field doesn't exist or has no values.
|
||||
//
|
||||
// Example:
|
||||
// lens := form.AtValue("username")
|
||||
// value := lens.Get(form) // Returns Option[string]
|
||||
// updated := lens.Set(O.Some("newuser"))(form)
|
||||
AtValue = F.Flow2(
|
||||
AtValues,
|
||||
composeHead,
|
||||
)
|
||||
)
|
||||
|
||||
// WithValue creates a [FormBuilder] for a certain field
|
||||
// WithValue creates an Endomorphism that sets a form field to a specific value.
|
||||
// It returns a curried function that takes the field name first, then the value,
|
||||
// and finally returns a transformation function.
|
||||
//
|
||||
// The transformation is immutable - it creates a new url.Values rather than
|
||||
// modifying the input.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Set a single field
|
||||
// form := form.WithValue("username")("john")(form.Default)
|
||||
//
|
||||
// // Compose multiple fields
|
||||
// form := F.Pipe3(
|
||||
// form.Default,
|
||||
// form.WithValue("username")("john"),
|
||||
// form.WithValue("email")("john@example.com"),
|
||||
// form.WithValue("age")("30"),
|
||||
// )
|
||||
func WithValue(name string) func(value string) Endomorphism {
|
||||
return F.Flow2(
|
||||
O.Of[string],
|
||||
@@ -68,7 +187,21 @@ func WithValue(name string) func(value string) Endomorphism {
|
||||
)
|
||||
}
|
||||
|
||||
// WithoutValue creates a [FormBuilder] that removes a field
|
||||
// WithoutValue creates an Endomorphism that removes a form field.
|
||||
// The transformation is immutable - it creates a new url.Values rather than
|
||||
// modifying the input.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Remove a field
|
||||
// updated := form.WithoutValue("age")(form)
|
||||
//
|
||||
// // Compose with other operations
|
||||
// form := F.Pipe2(
|
||||
// existingForm,
|
||||
// form.WithValue("username")("john"),
|
||||
// form.WithoutValue("password"),
|
||||
// )
|
||||
func WithoutValue(name string) Endomorphism {
|
||||
return AtValue(name).Set(noField)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package form
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
@@ -91,3 +92,448 @@ func TestFormField(t *testing.T) {
|
||||
assert.Equal(t, O.Of("v1"), l1.Get(v2))
|
||||
assert.Equal(t, O.Of("v2"), l2.Get(v2))
|
||||
}
|
||||
|
||||
// TestWithValue tests the WithValue function
|
||||
func TestWithValue(t *testing.T) {
|
||||
t.Run("sets a single value", func(t *testing.T) {
|
||||
form := WithValue("key")("value")(Default)
|
||||
assert.Equal(t, "value", form.Get("key"))
|
||||
})
|
||||
|
||||
t.Run("creates immutable transformation", func(t *testing.T) {
|
||||
original := Default
|
||||
modified := WithValue("key")("value")(original)
|
||||
|
||||
assert.False(t, valuesEq.Equals(original, modified))
|
||||
assert.Equal(t, "", original.Get("key"))
|
||||
assert.Equal(t, "value", modified.Get("key"))
|
||||
})
|
||||
|
||||
t.Run("overwrites existing value", func(t *testing.T) {
|
||||
form := WithValue("key")("value1")(Default)
|
||||
updated := WithValue("key")("value2")(form)
|
||||
|
||||
assert.Equal(t, "value2", updated.Get("key"))
|
||||
assert.Equal(t, "value1", form.Get("key"))
|
||||
})
|
||||
|
||||
t.Run("composes multiple values", func(t *testing.T) {
|
||||
form := F.Pipe3(
|
||||
Default,
|
||||
WithValue("key1")("value1"),
|
||||
WithValue("key2")("value2"),
|
||||
WithValue("key3")("value3"),
|
||||
)
|
||||
|
||||
assert.Equal(t, "value1", form.Get("key1"))
|
||||
assert.Equal(t, "value2", form.Get("key2"))
|
||||
assert.Equal(t, "value3", form.Get("key3"))
|
||||
})
|
||||
|
||||
t.Run("handles empty string values", func(t *testing.T) {
|
||||
form := WithValue("key")("")(Default)
|
||||
assert.Equal(t, "", form.Get("key"))
|
||||
assert.True(t, form.Has("key"))
|
||||
})
|
||||
|
||||
t.Run("handles special characters in keys", func(t *testing.T) {
|
||||
form := F.Pipe2(
|
||||
Default,
|
||||
WithValue("key-with-dash")("value1"),
|
||||
WithValue("key_with_underscore")("value2"),
|
||||
)
|
||||
|
||||
assert.Equal(t, "value1", form.Get("key-with-dash"))
|
||||
assert.Equal(t, "value2", form.Get("key_with_underscore"))
|
||||
})
|
||||
}
|
||||
|
||||
// TestWithoutValue tests the WithoutValue function
|
||||
func TestWithoutValue(t *testing.T) {
|
||||
t.Run("clears field value", func(t *testing.T) {
|
||||
form := WithValue("key")("value")(Default)
|
||||
updated := WithoutValue("key")(form)
|
||||
|
||||
// WithoutValue sets the field to an empty array, not removing it entirely
|
||||
assert.Equal(t, "", updated.Get("key"))
|
||||
// The field still exists but with empty values
|
||||
values := updated["key"]
|
||||
assert.Equal(t, 0, len(values))
|
||||
})
|
||||
|
||||
t.Run("is idempotent", func(t *testing.T) {
|
||||
form := WithValue("key")("value")(Default)
|
||||
removed1 := WithoutValue("key")(form)
|
||||
removed2 := WithoutValue("key")(removed1)
|
||||
|
||||
assert.True(t, valuesEq.Equals(removed1, removed2))
|
||||
})
|
||||
|
||||
t.Run("does not affect other fields", func(t *testing.T) {
|
||||
form := F.Pipe2(
|
||||
Default,
|
||||
WithValue("key1")("value1"),
|
||||
WithValue("key2")("value2"),
|
||||
)
|
||||
updated := WithoutValue("key1")(form)
|
||||
|
||||
assert.Equal(t, "", updated.Get("key1"))
|
||||
assert.Equal(t, "value2", updated.Get("key2"))
|
||||
})
|
||||
|
||||
t.Run("creates immutable transformation", func(t *testing.T) {
|
||||
form := WithValue("key")("value")(Default)
|
||||
updated := WithoutValue("key")(form)
|
||||
|
||||
assert.False(t, valuesEq.Equals(form, updated))
|
||||
assert.Equal(t, "value", form.Get("key"))
|
||||
assert.Equal(t, "", updated.Get("key"))
|
||||
})
|
||||
|
||||
t.Run("handles non-existent field", func(t *testing.T) {
|
||||
form := Default
|
||||
updated := WithoutValue("nonexistent")(form)
|
||||
|
||||
assert.True(t, valuesEq.Equals(form, updated))
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonoid tests the Monoid for Endomorphism
|
||||
func TestMonoid(t *testing.T) {
|
||||
t.Run("identity element", func(t *testing.T) {
|
||||
form := F.Pipe1(
|
||||
Default,
|
||||
WithValue("key")("value"),
|
||||
)
|
||||
|
||||
// Concatenating with identity should not change the result
|
||||
result := Monoid.Concat(Monoid.Empty(), WithValue("key")("value"))(Default)
|
||||
assert.True(t, valuesEq.Equals(form, result))
|
||||
})
|
||||
|
||||
t.Run("concatenates transformations", func(t *testing.T) {
|
||||
transform := Monoid.Concat(
|
||||
WithValue("key1")("value1"),
|
||||
WithValue("key2")("value2"),
|
||||
)
|
||||
result := transform(Default)
|
||||
|
||||
assert.Equal(t, "value1", result.Get("key1"))
|
||||
assert.Equal(t, "value2", result.Get("key2"))
|
||||
})
|
||||
|
||||
t.Run("concatenates multiple transformations", func(t *testing.T) {
|
||||
transform := Monoid.Concat(
|
||||
WithValue("key1")("value1"),
|
||||
Monoid.Concat(
|
||||
WithValue("key2")("value2"),
|
||||
WithValue("key3")("value3"),
|
||||
),
|
||||
)
|
||||
result := transform(Default)
|
||||
|
||||
assert.Equal(t, "value1", result.Get("key1"))
|
||||
assert.Equal(t, "value2", result.Get("key2"))
|
||||
assert.Equal(t, "value3", result.Get("key3"))
|
||||
})
|
||||
|
||||
t.Run("respects transformation order", func(t *testing.T) {
|
||||
// Monoid concatenation composes functions left-to-right
|
||||
// So the first transformation is applied first, then the second
|
||||
transform := Monoid.Concat(
|
||||
WithValue("key")("first"),
|
||||
WithValue("key")("second"),
|
||||
)
|
||||
result := transform(Default)
|
||||
|
||||
// The transformations are composed, so first is applied, then second overwrites it
|
||||
// But since Monoid.Concat composes endomorphisms, we need to check actual behavior
|
||||
assert.Equal(t, "first", result.Get("key"))
|
||||
})
|
||||
}
|
||||
|
||||
// TestValuesMonoid tests the ValuesMonoid
|
||||
func TestValuesMonoid(t *testing.T) {
|
||||
t.Run("identity element", func(t *testing.T) {
|
||||
form := url.Values{"key": []string{"value"}}
|
||||
result := ValuesMonoid.Concat(ValuesMonoid.Empty(), form)
|
||||
|
||||
assert.True(t, valuesEq.Equals(form, result))
|
||||
})
|
||||
|
||||
t.Run("concatenates disjoint forms", func(t *testing.T) {
|
||||
form1 := url.Values{"key1": []string{"value1"}}
|
||||
form2 := url.Values{"key2": []string{"value2"}}
|
||||
result := ValuesMonoid.Concat(form1, form2)
|
||||
|
||||
assert.Equal(t, "value1", result.Get("key1"))
|
||||
assert.Equal(t, "value2", result.Get("key2"))
|
||||
})
|
||||
|
||||
t.Run("concatenates arrays for same key", func(t *testing.T) {
|
||||
form1 := url.Values{"key": []string{"value1"}}
|
||||
form2 := url.Values{"key": []string{"value2"}}
|
||||
result := ValuesMonoid.Concat(form1, form2)
|
||||
|
||||
values := result["key"]
|
||||
assert.Equal(t, 2, len(values))
|
||||
assert.Equal(t, "value1", values[0])
|
||||
assert.Equal(t, "value2", values[1])
|
||||
})
|
||||
|
||||
t.Run("is associative", func(t *testing.T) {
|
||||
form1 := url.Values{"key": []string{"value1"}}
|
||||
form2 := url.Values{"key": []string{"value2"}}
|
||||
form3 := url.Values{"key": []string{"value3"}}
|
||||
|
||||
result1 := ValuesMonoid.Concat(ValuesMonoid.Concat(form1, form2), form3)
|
||||
result2 := ValuesMonoid.Concat(form1, ValuesMonoid.Concat(form2, form3))
|
||||
|
||||
assert.True(t, valuesEq.Equals(result1, result2))
|
||||
})
|
||||
}
|
||||
|
||||
// TestAtValues tests the AtValues lens
|
||||
func TestAtValues(t *testing.T) {
|
||||
t.Run("gets values array", func(t *testing.T) {
|
||||
form := url.Values{"key": []string{"value1", "value2"}}
|
||||
lens := AtValues("key")
|
||||
|
||||
result := lens.Get(form)
|
||||
assert.True(t, O.IsSome(result))
|
||||
values := O.GetOrElse(F.Constant([]string{}))(result)
|
||||
assert.Equal(t, 2, len(values))
|
||||
assert.Equal(t, "value1", values[0])
|
||||
assert.Equal(t, "value2", values[1])
|
||||
})
|
||||
|
||||
t.Run("returns None for non-existent key", func(t *testing.T) {
|
||||
lens := AtValues("nonexistent")
|
||||
result := lens.Get(Default)
|
||||
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("sets values array", func(t *testing.T) {
|
||||
lens := AtValues("key")
|
||||
form := lens.Set(O.Some([]string{"value1", "value2"}))(Default)
|
||||
|
||||
values := form["key"]
|
||||
assert.Equal(t, 2, len(values))
|
||||
assert.Equal(t, "value1", values[0])
|
||||
assert.Equal(t, "value2", values[1])
|
||||
})
|
||||
|
||||
t.Run("removes field with None", func(t *testing.T) {
|
||||
form := url.Values{"key": []string{"value"}}
|
||||
lens := AtValues("key")
|
||||
updated := lens.Set(O.None[[]string]())(form)
|
||||
|
||||
assert.False(t, updated.Has("key"))
|
||||
})
|
||||
|
||||
t.Run("creates immutable transformation", func(t *testing.T) {
|
||||
form := url.Values{"key": []string{"value1"}}
|
||||
lens := AtValues("key")
|
||||
updated := lens.Set(O.Some([]string{"value2"}))(form)
|
||||
|
||||
assert.False(t, valuesEq.Equals(form, updated))
|
||||
assert.Equal(t, "value1", form.Get("key"))
|
||||
assert.Equal(t, "value2", updated.Get("key"))
|
||||
})
|
||||
}
|
||||
|
||||
// TestAtValue tests the AtValue lens
|
||||
func TestAtValue(t *testing.T) {
|
||||
t.Run("gets first value", func(t *testing.T) {
|
||||
form := url.Values{"key": []string{"value1", "value2"}}
|
||||
lens := AtValue("key")
|
||||
|
||||
result := lens.Get(form)
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, "value1", O.GetOrElse(F.Constant(""))(result))
|
||||
})
|
||||
|
||||
t.Run("returns None for non-existent key", func(t *testing.T) {
|
||||
lens := AtValue("nonexistent")
|
||||
result := lens.Get(Default)
|
||||
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("returns None for empty array", func(t *testing.T) {
|
||||
form := url.Values{"key": []string{}}
|
||||
lens := AtValue("key")
|
||||
result := lens.Get(form)
|
||||
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("sets first value", func(t *testing.T) {
|
||||
lens := AtValue("key")
|
||||
form := lens.Set(O.Some("value"))(Default)
|
||||
|
||||
assert.Equal(t, "value", form.Get("key"))
|
||||
})
|
||||
|
||||
t.Run("replaces first value in array", func(t *testing.T) {
|
||||
form := url.Values{"key": []string{"old1", "old2"}}
|
||||
lens := AtValue("key")
|
||||
updated := lens.Set(O.Some("new"))(form)
|
||||
|
||||
values := updated["key"]
|
||||
// AtValue modifies the head of the array, keeping other elements
|
||||
assert.Equal(t, 2, len(values))
|
||||
assert.Equal(t, "new", values[0])
|
||||
assert.Equal(t, "old2", values[1])
|
||||
})
|
||||
|
||||
t.Run("clears field with None", func(t *testing.T) {
|
||||
form := url.Values{"key": []string{"value"}}
|
||||
lens := AtValue("key")
|
||||
updated := lens.Set(O.None[string]())(form)
|
||||
|
||||
// Setting to None creates an empty array, not removing the key
|
||||
values := updated["key"]
|
||||
assert.Equal(t, 0, len(values))
|
||||
})
|
||||
}
|
||||
|
||||
// Example tests demonstrating package usage
|
||||
|
||||
// ExampleWithValue demonstrates how to set form field values
|
||||
func ExampleWithValue() {
|
||||
// Create a form with a single field
|
||||
form := WithValue("username")("john")(Default)
|
||||
fmt.Println(form.Get("username"))
|
||||
// Output: john
|
||||
}
|
||||
|
||||
// ExampleWithValue_composition demonstrates composing multiple field assignments
|
||||
func ExampleWithValue_composition() {
|
||||
// Build a form with multiple fields using Pipe
|
||||
form := F.Pipe3(
|
||||
Default,
|
||||
WithValue("username")("john"),
|
||||
WithValue("email")("john@example.com"),
|
||||
WithValue("age")("30"),
|
||||
)
|
||||
|
||||
fmt.Println(form.Get("username"))
|
||||
fmt.Println(form.Get("email"))
|
||||
fmt.Println(form.Get("age"))
|
||||
// Output:
|
||||
// john
|
||||
// john@example.com
|
||||
// 30
|
||||
}
|
||||
|
||||
// ExampleWithoutValue demonstrates clearing a form field value
|
||||
func ExampleWithoutValue() {
|
||||
// Create a form and then clear a field
|
||||
form := F.Pipe2(
|
||||
Default,
|
||||
WithValue("username")("john"),
|
||||
WithValue("password")("secret"),
|
||||
)
|
||||
|
||||
// Clear the password field (sets it to empty array)
|
||||
sanitized := WithoutValue("password")(form)
|
||||
|
||||
fmt.Println(sanitized.Get("username"))
|
||||
fmt.Println(sanitized.Get("password"))
|
||||
// Output:
|
||||
// john
|
||||
//
|
||||
}
|
||||
|
||||
// ExampleAtValue demonstrates using the AtValue lens
|
||||
func ExampleAtValue() {
|
||||
form := WithValue("username")("john")(Default)
|
||||
|
||||
// Get a value using the lens
|
||||
lens := AtValue("username")
|
||||
value := lens.Get(form)
|
||||
|
||||
fmt.Println(O.IsSome(value))
|
||||
fmt.Println(O.GetOrElse(F.Constant("default"))(value))
|
||||
// Output:
|
||||
// true
|
||||
// john
|
||||
}
|
||||
|
||||
// ExampleAtValue_set demonstrates setting a value using the AtValue lens
|
||||
func ExampleAtValue_set() {
|
||||
form := WithValue("username")("john")(Default)
|
||||
|
||||
// Update the value using the lens
|
||||
lens := AtValue("username")
|
||||
updated := lens.Set(O.Some("jane"))(form)
|
||||
|
||||
fmt.Println(updated.Get("username"))
|
||||
// Output: jane
|
||||
}
|
||||
|
||||
// ExampleMonoid demonstrates combining form transformations
|
||||
func ExampleMonoid() {
|
||||
// Combine multiple transformations into one
|
||||
transform := Monoid.Concat(
|
||||
WithValue("field1")("value1"),
|
||||
WithValue("field2")("value2"),
|
||||
)
|
||||
|
||||
result := transform(Default)
|
||||
fmt.Println(result.Get("field1"))
|
||||
fmt.Println(result.Get("field2"))
|
||||
// Output:
|
||||
// value1
|
||||
// value2
|
||||
}
|
||||
|
||||
// ExampleValuesMonoid demonstrates merging form data
|
||||
func ExampleValuesMonoid() {
|
||||
form1 := url.Values{"key1": []string{"value1"}}
|
||||
form2 := url.Values{"key2": []string{"value2"}}
|
||||
|
||||
merged := ValuesMonoid.Concat(form1, form2)
|
||||
|
||||
fmt.Println(merged.Get("key1"))
|
||||
fmt.Println(merged.Get("key2"))
|
||||
// Output:
|
||||
// value1
|
||||
// value2
|
||||
}
|
||||
|
||||
// ExampleValuesMonoid_concatenation demonstrates array concatenation for same keys
|
||||
func ExampleValuesMonoid_concatenation() {
|
||||
form1 := url.Values{"tags": []string{"go"}}
|
||||
form2 := url.Values{"tags": []string{"functional"}}
|
||||
|
||||
merged := ValuesMonoid.Concat(form1, form2)
|
||||
|
||||
tags := merged["tags"]
|
||||
fmt.Println(len(tags))
|
||||
fmt.Println(tags[0])
|
||||
fmt.Println(tags[1])
|
||||
// Output:
|
||||
// 2
|
||||
// go
|
||||
// functional
|
||||
}
|
||||
|
||||
// ExampleAtValues demonstrates working with multiple values
|
||||
func ExampleAtValues() {
|
||||
form := url.Values{"tags": []string{"go", "functional", "programming"}}
|
||||
|
||||
lens := AtValues("tags")
|
||||
values := lens.Get(form)
|
||||
|
||||
if O.IsSome(values) {
|
||||
tags := O.GetOrElse(F.Constant([]string{}))(values)
|
||||
fmt.Println(len(tags))
|
||||
fmt.Println(tags[0])
|
||||
}
|
||||
// Output:
|
||||
// 3
|
||||
// go
|
||||
}
|
||||
|
||||
@@ -21,20 +21,68 @@ import (
|
||||
|
||||
type (
|
||||
ioApplicative[A, B any] struct{}
|
||||
|
||||
// IOApplicative represents the applicative functor type class for IO.
|
||||
// It combines the capabilities of Functor (Map) and Pointed (Of) with
|
||||
// the ability to apply wrapped functions to wrapped values (Ap).
|
||||
//
|
||||
// An applicative functor is a functor with two additional operations:
|
||||
// - Of: lifts a pure value into the IO context
|
||||
// - Ap: applies a wrapped function to a wrapped value
|
||||
//
|
||||
// This allows for function application within the IO context while maintaining
|
||||
// the computational structure. The Ap operation uses parallel execution by default
|
||||
// for better performance.
|
||||
//
|
||||
// Type parameters:
|
||||
// - A: the input type
|
||||
// - B: the output type
|
||||
IOApplicative[A, B any] = applicative.Applicative[A, B, IO[A], IO[B], IO[func(A) B]]
|
||||
)
|
||||
|
||||
// Of lifts a pure value into the IO context.
|
||||
// This is the pointed functor operation that wraps a value in an IO computation.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// app := io.Applicative[int, string]()
|
||||
// ioValue := app.Of(42) // IO[int] that returns 42
|
||||
// result := ioValue() // 42
|
||||
func (o *ioApplicative[A, B]) Of(a A) IO[A] {
|
||||
return Of(a)
|
||||
}
|
||||
|
||||
// Map transforms the result of an IO computation by applying a function to it.
|
||||
// This is the functor operation that allows mapping over wrapped values.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// app := io.Applicative[int, string]()
|
||||
// double := func(x int) int { return x * 2 }
|
||||
// ioValue := app.Of(21)
|
||||
// doubled := app.Map(double)(ioValue)
|
||||
// result := doubled() // 42
|
||||
func (o *ioApplicative[A, B]) Map(f func(A) B) Operator[A, B] {
|
||||
return Map(f)
|
||||
}
|
||||
|
||||
// Ap applies a wrapped function to a wrapped value, both in the IO context.
|
||||
// This operation uses parallel execution by default, running the function and
|
||||
// value computations concurrently for better performance.
|
||||
//
|
||||
// The Ap operation is useful for applying multi-argument functions in a curried
|
||||
// fashion within the IO context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// app := io.Applicative[int, int]()
|
||||
// add := func(a int) func(int) int {
|
||||
// return func(b int) int { return a + b }
|
||||
// }
|
||||
// ioFunc := app.Of(add(10)) // IO[func(int) int]
|
||||
// ioValue := app.Of(32) // IO[int]
|
||||
// result := app.Ap(ioValue)(ioFunc)
|
||||
// value := result() // 42
|
||||
func (o *ioApplicative[A, B]) Ap(fa IO[A]) Operator[func(A) B, B] {
|
||||
return Ap[B](fa)
|
||||
}
|
||||
@@ -43,10 +91,45 @@ func (o *ioApplicative[A, B]) Ap(fa IO[A]) Operator[func(A) B, B] {
|
||||
// This provides a structured way to access applicative operations (Of, Map, Ap)
|
||||
// for IO computations.
|
||||
//
|
||||
// Example:
|
||||
// The applicative pattern is useful when you need to:
|
||||
// - Apply functions with multiple arguments to wrapped values
|
||||
// - Combine multiple independent IO computations
|
||||
// - Maintain the computational structure while transforming values
|
||||
//
|
||||
// Type parameters:
|
||||
// - A: the input type for the applicative operations
|
||||
// - B: the output type for the applicative operations
|
||||
//
|
||||
// Example - Basic usage:
|
||||
//
|
||||
// app := io.Applicative[int, string]()
|
||||
// result := app.Map(strconv.Itoa)(app.Of(42))
|
||||
// value := result() // "42"
|
||||
//
|
||||
// Example - Applying curried functions:
|
||||
//
|
||||
// app := io.Applicative[int, int]()
|
||||
// add := func(a int) func(int) int {
|
||||
// return func(b int) int { return a + b }
|
||||
// }
|
||||
// // Create IO computations
|
||||
// ioFunc := io.Map(add)(app.Of(10)) // IO[func(int) int]
|
||||
// ioValue := app.Of(32) // IO[int]
|
||||
// // Apply the function to the value
|
||||
// result := app.Ap(ioValue)(ioFunc)
|
||||
// value := result() // 42
|
||||
//
|
||||
// Example - Combining multiple IO computations:
|
||||
//
|
||||
// app := io.Applicative[int, int]()
|
||||
// multiply := func(a int) func(int) int {
|
||||
// return func(b int) int { return a * b }
|
||||
// }
|
||||
// io1 := app.Of(6)
|
||||
// io2 := app.Of(7)
|
||||
// ioFunc := io.Map(multiply)(io1)
|
||||
// result := app.Ap(io2)(ioFunc)
|
||||
// value := result() // 42
|
||||
func Applicative[A, B any]() IOApplicative[A, B] {
|
||||
return &ioApplicative[A, B]{}
|
||||
}
|
||||
|
||||
360
v2/io/applicative_test.go
Normal file
360
v2/io/applicative_test.go
Normal file
@@ -0,0 +1,360 @@
|
||||
// Copyright (c) 2024 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package io
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestApplicativeOf tests the Of operation of the Applicative type class
|
||||
func TestApplicativeOf(t *testing.T) {
|
||||
app := Applicative[int, string]()
|
||||
|
||||
t.Run("wraps a value in IO context", func(t *testing.T) {
|
||||
ioValue := app.Of(42)
|
||||
result := ioValue()
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
|
||||
t.Run("wraps string value", func(t *testing.T) {
|
||||
app := Applicative[string, int]()
|
||||
ioValue := app.Of("hello")
|
||||
result := ioValue()
|
||||
assert.Equal(t, "hello", result)
|
||||
})
|
||||
|
||||
t.Run("wraps zero value", func(t *testing.T) {
|
||||
ioValue := app.Of(0)
|
||||
result := ioValue()
|
||||
assert.Equal(t, 0, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMap tests the Map operation of the Applicative type class
|
||||
func TestApplicativeMap(t *testing.T) {
|
||||
app := Applicative[int, int]()
|
||||
|
||||
t.Run("maps a function over IO value", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
ioValue := app.Of(21)
|
||||
result := app.Map(double)(ioValue)
|
||||
assert.Equal(t, 42, result())
|
||||
})
|
||||
|
||||
t.Run("maps type conversion", func(t *testing.T) {
|
||||
app := Applicative[int, string]()
|
||||
ioValue := app.Of(42)
|
||||
result := app.Map(strconv.Itoa)(ioValue)
|
||||
assert.Equal(t, "42", result())
|
||||
})
|
||||
|
||||
t.Run("maps identity function", func(t *testing.T) {
|
||||
identity := func(x int) int { return x }
|
||||
ioValue := app.Of(42)
|
||||
result := app.Map(identity)(ioValue)
|
||||
assert.Equal(t, 42, result())
|
||||
})
|
||||
|
||||
t.Run("maps constant function", func(t *testing.T) {
|
||||
constant := func(x int) int { return 100 }
|
||||
ioValue := app.Of(42)
|
||||
result := app.Map(constant)(ioValue)
|
||||
assert.Equal(t, 100, result())
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeAp tests the Ap operation of the Applicative type class
|
||||
func TestApplicativeAp(t *testing.T) {
|
||||
t.Run("applies wrapped function to wrapped value", func(t *testing.T) {
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
ioFunc := Of(add(10))
|
||||
ioValue := Of(32)
|
||||
result := Ap[int](ioValue)(ioFunc)
|
||||
assert.Equal(t, 42, result())
|
||||
})
|
||||
|
||||
t.Run("applies multiplication function", func(t *testing.T) {
|
||||
multiply := func(a int) func(int) int {
|
||||
return func(b int) int { return a * b }
|
||||
}
|
||||
ioFunc := Of(multiply(6))
|
||||
ioValue := Of(7)
|
||||
result := Ap[int](ioValue)(ioFunc)
|
||||
assert.Equal(t, 42, result())
|
||||
})
|
||||
|
||||
t.Run("applies function with zero", func(t *testing.T) {
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
ioFunc := Of(add(0))
|
||||
ioValue := Of(42)
|
||||
result := Ap[int](ioValue)(ioFunc)
|
||||
assert.Equal(t, 42, result())
|
||||
})
|
||||
|
||||
t.Run("applies with type conversion", func(t *testing.T) {
|
||||
toStringAndAppend := func(suffix string) func(int) string {
|
||||
return func(n int) string {
|
||||
return strconv.Itoa(n) + suffix
|
||||
}
|
||||
}
|
||||
ioFunc := Of(toStringAndAppend("!"))
|
||||
ioValue := Of(42)
|
||||
result := Ap[string](ioValue)(ioFunc)
|
||||
assert.Equal(t, "42!", result())
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeComposition tests composition of applicative operations
|
||||
func TestApplicativeComposition(t *testing.T) {
|
||||
app := Applicative[int, int]()
|
||||
|
||||
t.Run("composes Map and Of", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
result := F.Pipe1(
|
||||
app.Of(21),
|
||||
app.Map(double),
|
||||
)
|
||||
assert.Equal(t, 42, result())
|
||||
})
|
||||
|
||||
t.Run("composes multiple Map operations", func(t *testing.T) {
|
||||
app := Applicative[int, string]()
|
||||
double := func(x int) int { return x * 2 }
|
||||
toString := func(x int) string { return strconv.Itoa(x) }
|
||||
|
||||
result := F.Pipe2(
|
||||
app.Of(21),
|
||||
Map(double),
|
||||
app.Map(toString),
|
||||
)
|
||||
assert.Equal(t, "42", result())
|
||||
})
|
||||
|
||||
t.Run("composes Map and Ap", func(t *testing.T) {
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
|
||||
ioFunc := F.Pipe1(
|
||||
app.Of(5),
|
||||
Map(add),
|
||||
)
|
||||
ioValue := app.Of(16)
|
||||
|
||||
result := Ap[int](ioValue)(ioFunc)
|
||||
assert.Equal(t, 21, result())
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeLaws tests the applicative functor laws
|
||||
func TestApplicativeLaws(t *testing.T) {
|
||||
app := Applicative[int, int]()
|
||||
|
||||
t.Run("identity law: ap(Of(id), v) = v", func(t *testing.T) {
|
||||
identity := func(x int) int { return x }
|
||||
v := app.Of(42)
|
||||
|
||||
left := Ap[int](v)(Of(identity))
|
||||
right := v
|
||||
|
||||
assert.Equal(t, right(), left())
|
||||
})
|
||||
|
||||
t.Run("homomorphism law: ap(Of(f), Of(x)) = Of(f(x))", func(t *testing.T) {
|
||||
f := func(x int) int { return x * 2 }
|
||||
x := 21
|
||||
|
||||
left := Ap[int](app.Of(x))(Of(f))
|
||||
right := app.Of(f(x))
|
||||
|
||||
assert.Equal(t, right(), left())
|
||||
})
|
||||
|
||||
t.Run("interchange law: ap(u, Of(y)) = ap(Of(f => f(y)), u)", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
u := Of(double)
|
||||
y := 21
|
||||
|
||||
left := Ap[int](app.Of(y))(u)
|
||||
|
||||
applyY := func(f func(int) int) int { return f(y) }
|
||||
right := Ap[int](u)(Of(applyY))
|
||||
|
||||
assert.Equal(t, right(), left())
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeWithPipe tests applicative operations with pipe
|
||||
func TestApplicativeWithPipe(t *testing.T) {
|
||||
t.Run("pipes Of and Map", func(t *testing.T) {
|
||||
app := Applicative[int, string]()
|
||||
result := F.Pipe1(
|
||||
app.Of(42),
|
||||
app.Map(strconv.Itoa),
|
||||
)
|
||||
assert.Equal(t, "42", result())
|
||||
})
|
||||
|
||||
t.Run("pipes complex transformation", func(t *testing.T) {
|
||||
app := Applicative[int, int]()
|
||||
add10 := func(x int) int { return x + 10 }
|
||||
double := func(x int) int { return x * 2 }
|
||||
|
||||
result := F.Pipe2(
|
||||
app.Of(16),
|
||||
app.Map(add10),
|
||||
app.Map(double),
|
||||
)
|
||||
assert.Equal(t, 52, result())
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeWithUtils tests applicative with utility functions
|
||||
func TestApplicativeWithUtils(t *testing.T) {
|
||||
app := Applicative[int, int]()
|
||||
|
||||
t.Run("uses utils.Double", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
app.Of(21),
|
||||
app.Map(utils.Double),
|
||||
)
|
||||
assert.Equal(t, 42, result())
|
||||
})
|
||||
|
||||
t.Run("uses utils.Inc", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
app.Of(41),
|
||||
app.Map(utils.Inc),
|
||||
)
|
||||
assert.Equal(t, 42, result())
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMultipleArguments tests applying functions with multiple arguments
|
||||
func TestApplicativeMultipleArguments(t *testing.T) {
|
||||
app := Applicative[int, int]()
|
||||
|
||||
t.Run("applies curried two-argument function", func(t *testing.T) {
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
|
||||
// Create IO with curried function
|
||||
ioFunc := F.Pipe1(
|
||||
app.Of(10),
|
||||
Map(add),
|
||||
)
|
||||
|
||||
// Apply to second argument
|
||||
result := Ap[int](app.Of(32))(ioFunc)
|
||||
assert.Equal(t, 42, result())
|
||||
})
|
||||
|
||||
t.Run("applies curried three-argument function", func(t *testing.T) {
|
||||
add3 := func(a int) func(int) func(int) int {
|
||||
return func(b int) func(int) int {
|
||||
return func(c int) int {
|
||||
return a + b + c
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build up the computation step by step
|
||||
ioFunc1 := F.Pipe1(
|
||||
app.Of(10),
|
||||
Map(add3),
|
||||
)
|
||||
|
||||
ioFunc2 := Ap[func(int) int](app.Of(20))(ioFunc1)
|
||||
result := Ap[int](app.Of(12))(ioFunc2)
|
||||
|
||||
assert.Equal(t, 42, result())
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeParallelExecution tests that Ap uses parallel execution
|
||||
func TestApplicativeParallelExecution(t *testing.T) {
|
||||
t.Run("executes function and value in parallel", func(t *testing.T) {
|
||||
// This test verifies that both computations happen
|
||||
// The actual parallelism is tested by the implementation
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
|
||||
ioFunc := Of(add(20))
|
||||
ioValue := Of(22)
|
||||
|
||||
result := Ap[int](ioValue)(ioFunc)
|
||||
assert.Equal(t, 42, result())
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeInstance tests that Applicative returns a valid instance
|
||||
func TestApplicativeInstance(t *testing.T) {
|
||||
t.Run("returns non-nil instance", func(t *testing.T) {
|
||||
app := Applicative[int, string]()
|
||||
assert.NotNil(t, app)
|
||||
})
|
||||
|
||||
t.Run("multiple calls return independent instances", func(t *testing.T) {
|
||||
app1 := Applicative[int, string]()
|
||||
app2 := Applicative[int, string]()
|
||||
|
||||
// Both should work independently
|
||||
result1 := app1.Of(42)
|
||||
result2 := app2.Of(43)
|
||||
|
||||
assert.Equal(t, 42, result1())
|
||||
assert.Equal(t, 43, result2())
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeWithDifferentTypes tests applicative with various type combinations
|
||||
func TestApplicativeWithDifferentTypes(t *testing.T) {
|
||||
t.Run("int to string", func(t *testing.T) {
|
||||
app := Applicative[int, string]()
|
||||
result := app.Map(strconv.Itoa)(app.Of(42))
|
||||
assert.Equal(t, "42", result())
|
||||
})
|
||||
|
||||
t.Run("string to int", func(t *testing.T) {
|
||||
app := Applicative[string, int]()
|
||||
toLength := func(s string) int { return len(s) }
|
||||
result := app.Map(toLength)(app.Of("hello"))
|
||||
assert.Equal(t, 5, result())
|
||||
})
|
||||
|
||||
t.Run("bool to string", func(t *testing.T) {
|
||||
app := Applicative[bool, string]()
|
||||
toString := func(b bool) string {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
result := app.Map(toString)(app.Of(true))
|
||||
assert.Equal(t, "true", result())
|
||||
})
|
||||
}
|
||||
@@ -13,6 +13,51 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package file provides IO operations for file system interactions.
|
||||
//
|
||||
// This package offers functional wrappers around common file operations,
|
||||
// returning IO monads that encapsulate side effects. All operations are
|
||||
// lazy and only execute when the returned IO is invoked.
|
||||
//
|
||||
// # Core Operations
|
||||
//
|
||||
// The package provides two main operations:
|
||||
// - Close: Safely close io.Closer resources
|
||||
// - Remove: Remove files from the file system
|
||||
//
|
||||
// Both operations ignore errors and return the original input, making them
|
||||
// suitable for cleanup operations where errors should not interrupt the flow.
|
||||
//
|
||||
// # Basic Usage
|
||||
//
|
||||
// // Close a file
|
||||
// file, _ := os.Open("data.txt")
|
||||
// closeIO := file.Close(file)
|
||||
// closeIO() // Closes the file, ignoring any error
|
||||
//
|
||||
// // Remove a file
|
||||
// removeIO := file.Remove("temp.txt")
|
||||
// removeIO() // Removes the file, ignoring any error
|
||||
//
|
||||
// # Composition with IO
|
||||
//
|
||||
// These operations can be composed with other IO operations:
|
||||
//
|
||||
// result := pipe.Pipe2(
|
||||
// openFile("data.txt"),
|
||||
// io.ChainFirst(processFile),
|
||||
// io.Chain(file.Close),
|
||||
// )
|
||||
//
|
||||
// # Error Handling
|
||||
//
|
||||
// Both Close and Remove intentionally ignore errors. This design is suitable
|
||||
// for cleanup operations where:
|
||||
// - The operation is best-effort
|
||||
// - Errors should not interrupt the program flow
|
||||
// - The resource state is not critical
|
||||
//
|
||||
// For operations requiring error handling, use ioeither or ioresult instead.
|
||||
package file
|
||||
|
||||
import (
|
||||
@@ -22,7 +67,36 @@ import (
|
||||
IO "github.com/IBM/fp-go/v2/io"
|
||||
)
|
||||
|
||||
// Close closes a closeable resource and ignores a potential error
|
||||
// Close closes a closeable resource and ignores any potential error.
|
||||
// Returns an IO that, when executed, closes the resource and returns it.
|
||||
//
|
||||
// This function is useful for cleanup operations where errors can be safely
|
||||
// ignored, such as in defer statements or resource cleanup chains.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: Any type that implements io.Closer
|
||||
//
|
||||
// Parameters:
|
||||
// - r: The resource to close
|
||||
//
|
||||
// Returns:
|
||||
// - IO[R]: An IO computation that closes the resource and returns it
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// file, _ := os.Open("data.txt")
|
||||
// defer file.Close(file)() // Close when function returns
|
||||
//
|
||||
// Example with IO composition:
|
||||
//
|
||||
// result := pipe.Pipe3(
|
||||
// openFile("data.txt"),
|
||||
// io.Chain(readContent),
|
||||
// io.ChainFirst(file.Close),
|
||||
// )
|
||||
//
|
||||
// Note: The #nosec comment is intentional - errors are deliberately ignored
|
||||
// for cleanup operations where failure should not interrupt the flow.
|
||||
func Close[R io.Closer](r R) IO.IO[R] {
|
||||
return func() R {
|
||||
r.Close() // #nosec: G104
|
||||
@@ -30,7 +104,42 @@ func Close[R io.Closer](r R) IO.IO[R] {
|
||||
}
|
||||
}
|
||||
|
||||
// Remove removes a resource and ignores a potential error
|
||||
// Remove removes a file or directory and ignores any potential error.
|
||||
// Returns an IO that, when executed, removes the named file or directory
|
||||
// and returns the name.
|
||||
//
|
||||
// This function is useful for cleanup operations where errors can be safely
|
||||
// ignored, such as removing temporary files or cache directories.
|
||||
//
|
||||
// Parameters:
|
||||
// - name: The path to the file or directory to remove
|
||||
//
|
||||
// Returns:
|
||||
// - IO[string]: An IO computation that removes the file and returns the name
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// cleanup := file.Remove("temp.txt")
|
||||
// cleanup() // Removes temp.txt, ignoring any error
|
||||
//
|
||||
// Example with multiple files:
|
||||
//
|
||||
// cleanup := pipe.Pipe2(
|
||||
// file.Remove("temp1.txt"),
|
||||
// io.ChainTo(file.Remove("temp2.txt")),
|
||||
// )
|
||||
// cleanup() // Removes both files
|
||||
//
|
||||
// Example in defer:
|
||||
//
|
||||
// tempFile := "temp.txt"
|
||||
// defer file.Remove(tempFile)()
|
||||
// // ... use tempFile ...
|
||||
//
|
||||
// Note: The #nosec comment is intentional - errors are deliberately ignored
|
||||
// for cleanup operations where failure should not interrupt the flow.
|
||||
// This function only removes the named file or empty directory. To remove
|
||||
// a directory and its contents, use os.RemoveAll wrapped in an IO.
|
||||
func Remove(name string) IO.IO[string] {
|
||||
return func() string {
|
||||
os.Remove(name) // #nosec: G104
|
||||
|
||||
405
v2/io/file/file_test.go
Normal file
405
v2/io/file/file_test.go
Normal file
@@ -0,0 +1,405 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
IO "github.com/IBM/fp-go/v2/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// mockCloser is a mock implementation of io.Closer for testing
|
||||
type mockCloser struct {
|
||||
closed bool
|
||||
closeErr error
|
||||
closeFunc func() error
|
||||
}
|
||||
|
||||
func (m *mockCloser) Close() error {
|
||||
m.closed = true
|
||||
if m.closeFunc != nil {
|
||||
return m.closeFunc()
|
||||
}
|
||||
return m.closeErr
|
||||
}
|
||||
|
||||
// TestClose_WithMockCloser tests the Close function with a mock closer
|
||||
func TestClose_WithMockCloser(t *testing.T) {
|
||||
t.Run("closes resource successfully", func(t *testing.T) {
|
||||
mock := &mockCloser{}
|
||||
closeIO := Close(mock)
|
||||
|
||||
result := closeIO()
|
||||
|
||||
assert.True(t, mock.closed, "resource should be closed")
|
||||
assert.Equal(t, mock, result, "should return the same resource")
|
||||
})
|
||||
|
||||
t.Run("ignores close error", func(t *testing.T) {
|
||||
mock := &mockCloser{
|
||||
closeErr: fmt.Errorf("close error"),
|
||||
}
|
||||
closeIO := Close(mock)
|
||||
|
||||
// Should not panic even with error
|
||||
result := closeIO()
|
||||
|
||||
assert.True(t, mock.closed, "resource should be closed despite error")
|
||||
assert.Equal(t, mock, result, "should return the same resource")
|
||||
})
|
||||
|
||||
t.Run("can be called multiple times", func(t *testing.T) {
|
||||
mock := &mockCloser{}
|
||||
closeIO := Close(mock)
|
||||
|
||||
result1 := closeIO()
|
||||
result2 := closeIO()
|
||||
|
||||
assert.True(t, mock.closed, "resource should be closed")
|
||||
assert.Equal(t, result1, result2, "should return same resource each time")
|
||||
})
|
||||
}
|
||||
|
||||
// TestClose_WithBytesBuffer tests Close with bytes.Buffer (implements io.Closer)
|
||||
func TestClose_WithBytesBuffer(t *testing.T) {
|
||||
t.Run("closes bytes.Buffer", func(t *testing.T) {
|
||||
buf := bytes.NewBuffer([]byte("test data"))
|
||||
closeIO := Close(io.NopCloser(buf))
|
||||
|
||||
result := closeIO()
|
||||
|
||||
assert.NotNil(t, result, "should return the closer")
|
||||
})
|
||||
}
|
||||
|
||||
// TestClose_WithFile tests Close with actual file
|
||||
func TestClose_WithFile(t *testing.T) {
|
||||
t.Run("closes real file", func(t *testing.T) {
|
||||
// Create a temporary file
|
||||
tmpFile, err := os.CreateTemp("", "test-close-*.txt")
|
||||
require.NoError(t, err)
|
||||
tmpPath := tmpFile.Name()
|
||||
defer os.Remove(tmpPath)
|
||||
|
||||
// Write some data
|
||||
_, err = tmpFile.WriteString("test data")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Close using our function
|
||||
closeIO := Close(tmpFile)
|
||||
result := closeIO()
|
||||
|
||||
assert.Equal(t, tmpFile, result, "should return the same file")
|
||||
|
||||
// Verify file is closed by trying to write (should fail)
|
||||
_, err = tmpFile.WriteString("more data")
|
||||
assert.Error(t, err, "writing to closed file should fail")
|
||||
})
|
||||
}
|
||||
|
||||
// TestClose_Composition tests Close in IO composition
|
||||
func TestClose_Composition(t *testing.T) {
|
||||
t.Run("composes with other IO operations", func(t *testing.T) {
|
||||
mock := &mockCloser{}
|
||||
|
||||
// Create a pipeline that uses the resource and then closes it
|
||||
step1 := IO.Of(mock)
|
||||
step2 := IO.Map(func(m *mockCloser) *mockCloser {
|
||||
// Simulate using the resource
|
||||
return m
|
||||
})(step1)
|
||||
pipeline := IO.Chain(Close[*mockCloser])(step2)
|
||||
|
||||
result := pipeline()
|
||||
|
||||
assert.True(t, mock.closed, "resource should be closed in pipeline")
|
||||
assert.Equal(t, mock, result, "should return the resource")
|
||||
})
|
||||
|
||||
t.Run("works with ChainFirst", func(t *testing.T) {
|
||||
mock := &mockCloser{}
|
||||
data := "test data"
|
||||
|
||||
// Process data and close resource as side effect
|
||||
pipeline := IO.ChainFirst(func(string) IO.IO[*mockCloser] {
|
||||
return Close(mock)
|
||||
})(IO.Of(data))
|
||||
|
||||
result := pipeline()
|
||||
|
||||
assert.True(t, mock.closed, "resource should be closed")
|
||||
assert.Equal(t, data, result, "should return original data")
|
||||
})
|
||||
}
|
||||
|
||||
// TestRemove_BasicOperation tests basic Remove functionality
|
||||
func TestRemove_BasicOperation(t *testing.T) {
|
||||
t.Run("removes existing file", func(t *testing.T) {
|
||||
// Create a temporary file
|
||||
tmpFile, err := os.CreateTemp("", "test-remove-*.txt")
|
||||
require.NoError(t, err)
|
||||
tmpPath := tmpFile.Name()
|
||||
tmpFile.Close()
|
||||
|
||||
// Verify file exists
|
||||
_, err = os.Stat(tmpPath)
|
||||
require.NoError(t, err, "file should exist before removal")
|
||||
|
||||
// Remove using our function
|
||||
removeIO := Remove(tmpPath)
|
||||
result := removeIO()
|
||||
|
||||
assert.Equal(t, tmpPath, result, "should return the file path")
|
||||
|
||||
// Verify file is removed
|
||||
_, err = os.Stat(tmpPath)
|
||||
assert.True(t, os.IsNotExist(err), "file should not exist after removal")
|
||||
})
|
||||
|
||||
t.Run("ignores error for non-existent file", func(t *testing.T) {
|
||||
nonExistentPath := filepath.Join(os.TempDir(), "non-existent-file-12345.txt")
|
||||
|
||||
// Should not panic even if file doesn't exist
|
||||
removeIO := Remove(nonExistentPath)
|
||||
result := removeIO()
|
||||
|
||||
assert.Equal(t, nonExistentPath, result, "should return the path")
|
||||
})
|
||||
|
||||
t.Run("removes empty directory", func(t *testing.T) {
|
||||
// Create a temporary directory
|
||||
tmpDir, err := os.MkdirTemp("", "test-remove-dir-*")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify directory exists
|
||||
_, err = os.Stat(tmpDir)
|
||||
require.NoError(t, err, "directory should exist before removal")
|
||||
|
||||
// Remove using our function
|
||||
removeIO := Remove(tmpDir)
|
||||
result := removeIO()
|
||||
|
||||
assert.Equal(t, tmpDir, result, "should return the directory path")
|
||||
|
||||
// Verify directory is removed
|
||||
_, err = os.Stat(tmpDir)
|
||||
assert.True(t, os.IsNotExist(err), "directory should not exist after removal")
|
||||
})
|
||||
|
||||
t.Run("ignores error for non-empty directory", func(t *testing.T) {
|
||||
// Create a temporary directory with a file
|
||||
tmpDir, err := os.MkdirTemp("", "test-remove-nonempty-*")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tmpDir) // Cleanup
|
||||
|
||||
tmpFile := filepath.Join(tmpDir, "file.txt")
|
||||
err = os.WriteFile(tmpFile, []byte("data"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should not panic even if directory is not empty
|
||||
removeIO := Remove(tmpDir)
|
||||
result := removeIO()
|
||||
|
||||
assert.Equal(t, tmpDir, result, "should return the path")
|
||||
|
||||
// Directory should still exist (os.Remove doesn't remove non-empty dirs)
|
||||
_, err = os.Stat(tmpDir)
|
||||
assert.NoError(t, err, "non-empty directory should still exist")
|
||||
})
|
||||
}
|
||||
|
||||
// TestRemove_Composition tests Remove in IO composition
|
||||
func TestRemove_Composition(t *testing.T) {
|
||||
t.Run("composes with other IO operations", func(t *testing.T) {
|
||||
// Create a temporary file
|
||||
tmpFile, err := os.CreateTemp("", "test-compose-*.txt")
|
||||
require.NoError(t, err)
|
||||
tmpPath := tmpFile.Name()
|
||||
tmpFile.Close()
|
||||
|
||||
// Create a pipeline that processes and removes the file
|
||||
step1 := IO.Of(tmpPath)
|
||||
step2 := IO.Map(func(path string) string {
|
||||
// Simulate processing
|
||||
return path
|
||||
})(step1)
|
||||
pipeline := IO.Chain(Remove)(step2)
|
||||
|
||||
result := pipeline()
|
||||
|
||||
assert.Equal(t, tmpPath, result, "should return the path")
|
||||
|
||||
// Verify file is removed
|
||||
_, err = os.Stat(tmpPath)
|
||||
assert.True(t, os.IsNotExist(err), "file should be removed")
|
||||
})
|
||||
|
||||
t.Run("removes multiple files in sequence", func(t *testing.T) {
|
||||
// Create temporary files
|
||||
tmpFile1, err := os.CreateTemp("", "test-multi-1-*.txt")
|
||||
require.NoError(t, err)
|
||||
tmpPath1 := tmpFile1.Name()
|
||||
tmpFile1.Close()
|
||||
|
||||
tmpFile2, err := os.CreateTemp("", "test-multi-2-*.txt")
|
||||
require.NoError(t, err)
|
||||
tmpPath2 := tmpFile2.Name()
|
||||
tmpFile2.Close()
|
||||
|
||||
// Remove both files in sequence
|
||||
pipeline := IO.ChainTo[string](Remove(tmpPath2))(Remove(tmpPath1))
|
||||
|
||||
result := pipeline()
|
||||
|
||||
assert.Equal(t, tmpPath2, result, "should return last path")
|
||||
|
||||
// Verify both files are removed
|
||||
_, err = os.Stat(tmpPath1)
|
||||
assert.True(t, os.IsNotExist(err), "first file should be removed")
|
||||
|
||||
_, err = os.Stat(tmpPath2)
|
||||
assert.True(t, os.IsNotExist(err), "second file should be removed")
|
||||
})
|
||||
}
|
||||
|
||||
// TestRemove_CanBeCalledMultipleTimes tests idempotency
|
||||
func TestRemove_CanBeCalledMultipleTimes(t *testing.T) {
|
||||
t.Run("calling remove multiple times is safe", func(t *testing.T) {
|
||||
// Create a temporary file
|
||||
tmpFile, err := os.CreateTemp("", "test-idempotent-*.txt")
|
||||
require.NoError(t, err)
|
||||
tmpPath := tmpFile.Name()
|
||||
tmpFile.Close()
|
||||
|
||||
removeIO := Remove(tmpPath)
|
||||
|
||||
// First call removes the file
|
||||
result1 := removeIO()
|
||||
assert.Equal(t, tmpPath, result1)
|
||||
|
||||
// Second call should not panic (file already removed)
|
||||
result2 := removeIO()
|
||||
assert.Equal(t, tmpPath, result2)
|
||||
|
||||
// Verify file is removed
|
||||
_, err = os.Stat(tmpPath)
|
||||
assert.True(t, os.IsNotExist(err), "file should be removed")
|
||||
})
|
||||
}
|
||||
|
||||
// TestCloseAndRemove_Together tests using both functions together
|
||||
func TestCloseAndRemove_Together(t *testing.T) {
|
||||
t.Run("close and remove file in sequence", func(t *testing.T) {
|
||||
// Create a temporary file
|
||||
tmpFile, err := os.CreateTemp("", "test-close-remove-*.txt")
|
||||
require.NoError(t, err)
|
||||
tmpPath := tmpFile.Name()
|
||||
|
||||
// Write some data
|
||||
_, err = tmpFile.WriteString("test data")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Close and remove in sequence
|
||||
pipeline := IO.Chain(func(f *os.File) IO.IO[string] {
|
||||
return Remove(f.Name())
|
||||
})(Close(tmpFile))
|
||||
|
||||
result := pipeline()
|
||||
|
||||
assert.Equal(t, tmpPath, result, "should return the path")
|
||||
|
||||
// Verify file is removed
|
||||
_, err = os.Stat(tmpPath)
|
||||
assert.True(t, os.IsNotExist(err), "file should be removed")
|
||||
})
|
||||
}
|
||||
|
||||
// TestClose_TypeSafety tests that Close works with different io.Closer types
|
||||
func TestClose_TypeSafety(t *testing.T) {
|
||||
t.Run("works with different closer types", func(t *testing.T) {
|
||||
// Test with different types that implement io.Closer
|
||||
types := []io.Closer{
|
||||
&mockCloser{},
|
||||
io.NopCloser(bytes.NewBuffer(nil)),
|
||||
}
|
||||
|
||||
for _, closer := range types {
|
||||
closeIO := Close(closer)
|
||||
result := closeIO()
|
||||
assert.Equal(t, closer, result, "should return the same closer")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Example_close demonstrates basic usage of Close
|
||||
func Example_close() {
|
||||
// Create a mock closer
|
||||
mock := &mockCloser{}
|
||||
|
||||
// Create an IO that closes the resource
|
||||
closeIO := Close(mock)
|
||||
|
||||
// Execute the IO
|
||||
result := closeIO()
|
||||
|
||||
fmt.Printf("Closed: %v\n", result.closed)
|
||||
// Output: Closed: true
|
||||
}
|
||||
|
||||
// Example_remove demonstrates basic usage of Remove
|
||||
func Example_remove() {
|
||||
// Create a temporary file
|
||||
tmpFile, _ := os.CreateTemp("", "example-*.txt")
|
||||
tmpPath := tmpFile.Name()
|
||||
tmpFile.Close()
|
||||
|
||||
// Create an IO that removes the file
|
||||
removeIO := Remove(tmpPath)
|
||||
|
||||
// Execute the IO
|
||||
path := removeIO()
|
||||
|
||||
// Check if file exists
|
||||
_, err := os.Stat(path)
|
||||
fmt.Printf("File removed: %v\n", os.IsNotExist(err))
|
||||
// Output: File removed: true
|
||||
}
|
||||
|
||||
// Example_closeAndRemove demonstrates using Close and Remove together
|
||||
func Example_closeAndRemove() {
|
||||
// Create a temporary file
|
||||
tmpFile, _ := os.CreateTemp("", "example-*.txt")
|
||||
|
||||
// Create a pipeline that closes and removes the file
|
||||
pipeline := IO.Chain(func(f *os.File) IO.IO[string] {
|
||||
return Remove(f.Name())
|
||||
})(Close(tmpFile))
|
||||
|
||||
// Execute the pipeline
|
||||
path := pipeline()
|
||||
|
||||
// Check if file exists
|
||||
_, err := os.Stat(path)
|
||||
fmt.Printf("File removed: %v\n", os.IsNotExist(err))
|
||||
// Output: File removed: true
|
||||
}
|
||||
260
v2/optics/codec/codec.go
Normal file
260
v2/optics/codec/codec.go
Normal file
@@ -0,0 +1,260 @@
|
||||
package codec
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
"github.com/IBM/fp-go/v2/array"
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readereither"
|
||||
R "github.com/IBM/fp-go/v2/reflect"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// typeImpl is the internal implementation of the Type interface.
|
||||
// It combines encoding, decoding, validation, and type checking capabilities.
|
||||
type typeImpl[A, O, I any] struct {
|
||||
name string
|
||||
is Reader[any, Result[A]]
|
||||
validate Validate[I, A]
|
||||
encode Encode[A, O]
|
||||
}
|
||||
|
||||
// MakeType creates a new Type with the given name, type checker, validator, and encoder.
|
||||
//
|
||||
// Parameters:
|
||||
// - name: A descriptive name for this type (used in error messages)
|
||||
// - is: A function that checks if a value is of type A
|
||||
// - validate: A function that validates and decodes input I to type A
|
||||
// - encode: A function that encodes type A to output O
|
||||
//
|
||||
// Returns a Type[A, O, I] that can both encode and decode values.
|
||||
func MakeType[A, O, I any](
|
||||
name string,
|
||||
is Reader[any, Result[A]],
|
||||
validate Validate[I, A],
|
||||
encode Encode[A, O],
|
||||
) Type[A, O, I] {
|
||||
return &typeImpl[A, O, I]{
|
||||
name: name,
|
||||
is: is,
|
||||
validate: validate,
|
||||
encode: encode,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate validates the input value in the context of a validation path.
|
||||
// Returns a Reader that takes a Context and produces a Validation result.
|
||||
func (t *typeImpl[A, O, I]) Validate(i I) Reader[Context, Validation[A]] {
|
||||
return t.validate(i)
|
||||
}
|
||||
|
||||
// Decode validates and decodes the input value, creating a new context with this type's name.
|
||||
// This is a convenience method that calls Validate with a fresh context.
|
||||
func (t *typeImpl[A, O, I]) Decode(i I) Validation[A] {
|
||||
return t.validate(i)(array.Of(validation.ContextEntry{Type: t.name, Actual: i}))
|
||||
}
|
||||
|
||||
// Encode transforms a value of type A into the output format O.
|
||||
func (t *typeImpl[A, O, I]) Encode(a A) O {
|
||||
return t.encode(a)
|
||||
}
|
||||
|
||||
// AsDecoder returns this Type as a Decoder interface.
|
||||
func (t *typeImpl[A, O, I]) AsDecoder() Decoder[I, A] {
|
||||
return t
|
||||
}
|
||||
|
||||
// AsEncoder returns this Type as an Encoder interface.
|
||||
func (t *typeImpl[A, O, I]) AsEncoder() Encoder[A, O] {
|
||||
return t
|
||||
}
|
||||
|
||||
// Name returns the descriptive name of this type.
|
||||
func (t *typeImpl[A, O, I]) Name() string {
|
||||
return t.name
|
||||
}
|
||||
|
||||
func (t *typeImpl[A, O, I]) Is(i any) Result[A] {
|
||||
return t.is(i)
|
||||
}
|
||||
|
||||
// Pipe composes two Types, creating a pipeline where:
|
||||
// - Decoding: I -> A -> B (decode with 'this', then validate with 'ab')
|
||||
// - Encoding: B -> A -> O (encode with 'ab', then encode with 'this')
|
||||
//
|
||||
// This allows building complex codecs from simpler ones.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// stringToInt := codec.MakeType(...) // Type[int, string, string]
|
||||
// intToPositive := codec.MakeType(...) // Type[PositiveInt, int, int]
|
||||
// composed := codec.Pipe(intToPositive)(stringToInt) // Type[PositiveInt, string, string]
|
||||
func Pipe[A, B, O, I any](ab Type[B, A, A]) func(Type[A, O, I]) Type[B, O, I] {
|
||||
return func(this Type[A, O, I]) Type[B, O, I] {
|
||||
return MakeType(
|
||||
fmt.Sprintf("Pipe(%s, %s)", this.Name(), ab.Name()),
|
||||
ab.Is,
|
||||
F.Flow2(
|
||||
this.Validate,
|
||||
readereither.Chain(ab.Validate),
|
||||
),
|
||||
F.Flow2(
|
||||
ab.Encode,
|
||||
this.Encode,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// isNil checks if a value is nil, handling both typed and untyped nil values.
|
||||
// It uses reflection to detect nil pointers, maps, slices, channels, functions, and interfaces.
|
||||
func isNil(x any) bool {
|
||||
if x == nil {
|
||||
return true
|
||||
}
|
||||
v := reflect.ValueOf(x)
|
||||
switch v.Kind() {
|
||||
case reflect.Ptr, reflect.Map, reflect.Slice, reflect.Chan, reflect.Func, reflect.Interface:
|
||||
return v.IsNil()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// isTypedNil checks if a value is nil and returns it as a typed nil pointer.
|
||||
// Returns Some(nil) if the value is nil, None otherwise.
|
||||
func isTypedNil[A any](x any) Result[*A] {
|
||||
if isNil(x) {
|
||||
return result.Of[*A](nil)
|
||||
}
|
||||
return result.Left[*A](errors.New("expecting nil"))
|
||||
}
|
||||
|
||||
func validateFromIs[A any](
|
||||
is ReaderResult[any, A],
|
||||
msg string,
|
||||
) Reader[any, Reader[Context, Validation[A]]] {
|
||||
return func(u any) Reader[Context, Validation[A]] {
|
||||
return F.Pipe2(
|
||||
u,
|
||||
is,
|
||||
result.Fold(
|
||||
validation.FailureWithError[A](u, msg),
|
||||
F.Flow2(
|
||||
validation.Success[A],
|
||||
reader.Of[Context],
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MakeNilType creates a Type that validates nil values.
|
||||
// It accepts any input and validates that it is nil, returning a typed nil pointer.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// nilType := codec.MakeNilType[string]()
|
||||
// result := nilType.Decode(nil) // Success: Right((*string)(nil))
|
||||
// result := nilType.Decode("not nil") // Failure: Left(errors)
|
||||
func Nil[A any]() Type[*A, *A, any] {
|
||||
|
||||
is := isTypedNil[A]
|
||||
|
||||
return MakeType(
|
||||
"nil",
|
||||
is,
|
||||
validateFromIs(is, "nil"),
|
||||
F.Identity[*A],
|
||||
)
|
||||
}
|
||||
|
||||
func MakeSimpleType[A any]() Type[A, A, any] {
|
||||
var zero A
|
||||
name := fmt.Sprintf("%T", zero)
|
||||
is := Is[A]()
|
||||
|
||||
return MakeType(
|
||||
name,
|
||||
is,
|
||||
validateFromIs(is, name),
|
||||
F.Identity[A],
|
||||
)
|
||||
}
|
||||
|
||||
func String() Type[string, string, any] {
|
||||
return MakeSimpleType[string]()
|
||||
}
|
||||
|
||||
func Int() Type[int, int, any] {
|
||||
return MakeSimpleType[int]()
|
||||
}
|
||||
|
||||
func Bool() Type[bool, bool, any] {
|
||||
return MakeSimpleType[bool]()
|
||||
}
|
||||
|
||||
func appendContext(key, typ string, actual any) Endomorphism[Context] {
|
||||
return A.Push(validation.ContextEntry{Key: key, Type: typ, Actual: actual})
|
||||
}
|
||||
|
||||
type validationPair[T any] = Pair[validation.Errors, T]
|
||||
|
||||
func pairToValidation[T any](p validationPair[T]) Validation[T] {
|
||||
errors, value := pair.Unpack(p)
|
||||
if A.IsNonEmpty(errors) {
|
||||
return either.Left[T](errors)
|
||||
}
|
||||
return either.Of[validation.Errors](value)
|
||||
}
|
||||
|
||||
func validateArray[T any](item Type[T, T, any]) func(u any) Reader[Context, Validation[[]T]] {
|
||||
|
||||
appendErrors := F.Flow2(
|
||||
A.Concat,
|
||||
pair.MapHead[[]T, validation.Errors],
|
||||
)
|
||||
|
||||
appendValues := F.Flow2(
|
||||
A.Push,
|
||||
pair.MapTail[validation.Errors, []T],
|
||||
)
|
||||
|
||||
itemName := item.Name()
|
||||
|
||||
return func(u any) Reader[Context, Validation[[]T]] {
|
||||
val := reflect.ValueOf(u)
|
||||
if !val.IsValid() {
|
||||
return validation.FailureWithMessage[[]T](val, "invalid value")
|
||||
}
|
||||
kind := val.Kind()
|
||||
|
||||
switch kind {
|
||||
case reflect.Array, reflect.Slice, reflect.String:
|
||||
|
||||
return func(c Context) Validation[[]T] {
|
||||
|
||||
return F.Pipe1(
|
||||
R.MonadReduceWithIndex(val, func(i int, p validationPair[[]T], v reflect.Value) validationPair[[]T] {
|
||||
return either.MonadFold[validation.Errors, T, Endomorphism[validationPair[[]T]]](
|
||||
item.Validate(u)(appendContext(strconv.Itoa(i), itemName, u)(c)),
|
||||
appendErrors,
|
||||
appendValues,
|
||||
)(p)
|
||||
}, validationPair[[]T]{}),
|
||||
pairToValidation,
|
||||
)
|
||||
}
|
||||
default:
|
||||
return validation.FailureWithMessage[[]T](val, fmt.Sprintf("type %s is not iterable", kind))
|
||||
}
|
||||
}
|
||||
}
|
||||
15
v2/optics/codec/codec_test.go
Normal file
15
v2/optics/codec/codec_test.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package codec
|
||||
|
||||
import (
|
||||
"log"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStringCoded(t *testing.T) {
|
||||
|
||||
sType := String()
|
||||
|
||||
res := sType.Decode(10)
|
||||
|
||||
log.Println(res)
|
||||
}
|
||||
57
v2/optics/codec/doc.go
Normal file
57
v2/optics/codec/doc.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// Package codec provides a functional approach to encoding and decoding data with validation.
|
||||
//
|
||||
// The codec package combines the concepts of encoders and decoders into a unified Type that can
|
||||
// both encode values to an output format and decode/validate values from an input format. This
|
||||
// is particularly useful for data serialization, API validation, and type-safe transformations.
|
||||
//
|
||||
// # Core Concepts
|
||||
//
|
||||
// Type[A, O, I]: A bidirectional codec that can:
|
||||
// - Decode input I to type A with validation
|
||||
// - Encode type A to output O
|
||||
// - Check if a value is of type A
|
||||
//
|
||||
// Validation: Decoding returns Either[Errors, A] which represents:
|
||||
// - Left(Errors): Validation failed with detailed error information
|
||||
// - Right(A): Successfully decoded and validated value
|
||||
//
|
||||
// Context: A stack of ContextEntry values that tracks the path through nested structures
|
||||
// during validation, providing detailed error messages.
|
||||
//
|
||||
// # Basic Usage
|
||||
//
|
||||
// Creating a simple type:
|
||||
//
|
||||
// nilType := codec.MakeNilType[string]()
|
||||
// result := nilType.Decode(nil) // Success
|
||||
// result := nilType.Decode("not nil") // Failure
|
||||
//
|
||||
// Composing types with Pipe:
|
||||
//
|
||||
// composed := codec.Pipe(typeB)(typeA)
|
||||
// // Decodes: I -> A -> B
|
||||
// // Encodes: B -> A -> O
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// Most functions use three type parameters:
|
||||
// - A: The domain type (the actual Go type being encoded/decoded)
|
||||
// - O: The output type for encoding
|
||||
// - I: The input type for decoding
|
||||
//
|
||||
// # Validation Errors
|
||||
//
|
||||
// ValidationError contains:
|
||||
// - Value: The actual value that failed validation
|
||||
// - Context: The path to the value in nested structures
|
||||
// - Message: Human-readable error description
|
||||
//
|
||||
// # Integration
|
||||
//
|
||||
// This package integrates with:
|
||||
// - optics/decoder: For decoding operations
|
||||
// - optics/encoder: For encoding operations
|
||||
// - either: For validation results
|
||||
// - option: For optional type checking
|
||||
// - reader: For context-dependent operations
|
||||
package codec
|
||||
83
v2/optics/codec/types.go
Normal file
83
v2/optics/codec/types.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package codec
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/optics/decoder"
|
||||
"github.com/IBM/fp-go/v2/optics/encoder"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerresult"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
type (
|
||||
ReaderResult[R, A any] = readerresult.ReaderResult[R, A]
|
||||
|
||||
// Lazy represents a lazily evaluated value.
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
|
||||
// Reader represents a computation that depends on an environment R and produces a value A.
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
|
||||
// Option represents an optional value that may or may not be present.
|
||||
Option[A any] = option.Option[A]
|
||||
|
||||
// Either represents a value that can be one of two types: Left (error) or Right (success).
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
|
||||
// Result represents a computation that may fail with an error.
|
||||
Result[A any] = result.Result[A]
|
||||
|
||||
// Codec combines a Decoder and an Encoder for bidirectional transformations.
|
||||
// It can decode input I to type A and encode type A to output O.
|
||||
Codec[I, O, A any] struct {
|
||||
Decode decoder.Decoder[I, A]
|
||||
Encode encoder.Encoder[O, A]
|
||||
}
|
||||
|
||||
Validation[A any] = validation.Validation[A]
|
||||
|
||||
Context = validation.Context
|
||||
|
||||
// Validate is a function that validates input I to produce type A.
|
||||
// It takes an input and returns a Reader that depends on the validation Context.
|
||||
Validate[I, A any] = Reader[I, Reader[Context, Validation[A]]]
|
||||
|
||||
// Decode is a function that decodes input I to type A with validation.
|
||||
// It returns a Validation result directly.
|
||||
Decode[I, A any] = Reader[I, Validation[A]]
|
||||
|
||||
// Encode is a function that encodes type A to output O.
|
||||
Encode[A, O any] = Reader[A, O]
|
||||
|
||||
// Decoder is an interface for types that can decode and validate input.
|
||||
Decoder[I, A any] interface {
|
||||
Name() string
|
||||
Validate(I) Reader[Context, Validation[A]]
|
||||
Decode(I) Validation[A]
|
||||
}
|
||||
|
||||
// Encoder is an interface for types that can encode values.
|
||||
Encoder[A, O any] interface {
|
||||
// Encode transforms a value of type A into output format O.
|
||||
Encode(A) O
|
||||
}
|
||||
// Type is a bidirectional codec that combines encoding, decoding, validation,
|
||||
// and type checking capabilities. It represents a complete specification of
|
||||
// how to work with a particular type.
|
||||
Type[A, O, I any] interface {
|
||||
Decoder[I, A]
|
||||
Encoder[A, O]
|
||||
AsDecoder() Decoder[I, A]
|
||||
AsEncoder() Encoder[A, O]
|
||||
Is(any) Result[A]
|
||||
}
|
||||
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
Pair[L, R any] = pair.Pair[L, R]
|
||||
)
|
||||
21
v2/optics/codec/validation.go
Normal file
21
v2/optics/codec/validation.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package codec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
func onTypeError(expType string) func(any) error {
|
||||
return func(u any) error {
|
||||
return fmt.Errorf("expecting type [%s] but got [%T]", expType, u)
|
||||
}
|
||||
}
|
||||
|
||||
// Is checks if a value can be converted to type T.
|
||||
// Returns Some(value) if the conversion succeeds, None otherwise.
|
||||
// This is a type-safe cast operation.
|
||||
func Is[T any]() func(any) Result[T] {
|
||||
var zero T
|
||||
return result.ToType[T](onTypeError(fmt.Sprintf("%T", zero)))
|
||||
}
|
||||
104
v2/optics/codec/validation/doc.go
Normal file
104
v2/optics/codec/validation/doc.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// Package validation provides functional validation types and operations for the codec system.
|
||||
//
|
||||
// This package implements a validation monad that accumulates errors during validation operations,
|
||||
// making it ideal for form validation, data parsing, and other scenarios where you want to collect
|
||||
// all validation errors rather than failing on the first error.
|
||||
//
|
||||
// # Core Concepts
|
||||
//
|
||||
// Validation[A]: Represents the result of a validation operation as Either[Errors, A]:
|
||||
// - Left(Errors): Validation failed with one or more errors
|
||||
// - Right(A): Successfully validated value of type A
|
||||
//
|
||||
// ValidationError: A detailed error type that includes:
|
||||
// - Value: The actual value that failed validation
|
||||
// - Context: The path through nested structures (e.g., "user.address.zipCode")
|
||||
// - Message: Human-readable error description
|
||||
// - Cause: Optional underlying error
|
||||
//
|
||||
// Context: A stack of ContextEntry values that tracks the validation path through
|
||||
// nested data structures, enabling precise error reporting.
|
||||
//
|
||||
// # Basic Usage
|
||||
//
|
||||
// Creating validation results:
|
||||
//
|
||||
// // Success case
|
||||
// valid := validation.Success(42)
|
||||
//
|
||||
// // Failure case
|
||||
// invalid := validation.Failures[int](validation.Errors{
|
||||
// &validation.ValidationError{
|
||||
// Value: "not a number",
|
||||
// Message: "expected integer",
|
||||
// Context: nil,
|
||||
// },
|
||||
// })
|
||||
//
|
||||
// Using with context:
|
||||
//
|
||||
// failWithMsg := validation.FailureWithMessage[int]("invalid", "must be positive")
|
||||
// result := failWithMsg([]validation.ContextEntry{
|
||||
// {Key: "age", Type: "int"},
|
||||
// })
|
||||
//
|
||||
// # Applicative Validation
|
||||
//
|
||||
// The validation type supports applicative operations, allowing you to combine
|
||||
// multiple validations and accumulate all errors:
|
||||
//
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Email string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// validateName := func(s string) validation.Validation[string] {
|
||||
// if len(s) > 0 {
|
||||
// return validation.Success(s)
|
||||
// }
|
||||
// return validation.Failures[string](/* error */)
|
||||
// }
|
||||
//
|
||||
// // Combine validations - all errors will be collected
|
||||
// result := validation.Ap(validation.Ap(validation.Ap(
|
||||
// validation.Of(func(name string) func(email string) func(age int) User {
|
||||
// return func(email string) func(age int) User {
|
||||
// return func(age int) User {
|
||||
// return User{name, email, age}
|
||||
// }
|
||||
// }
|
||||
// }),
|
||||
// )(validateName("")))(validateEmail("")))(validateAge(-1))
|
||||
//
|
||||
// # Error Formatting
|
||||
//
|
||||
// ValidationError implements custom formatting for detailed error messages:
|
||||
//
|
||||
// err := &ValidationError{
|
||||
// Value: "abc",
|
||||
// Context: []ContextEntry{{Key: "user"}, {Key: "age"}},
|
||||
// Message: "expected integer",
|
||||
// }
|
||||
//
|
||||
// fmt.Printf("%v", err) // at user.age: expected integer
|
||||
// fmt.Printf("%+v", err) // at user.age: expected integer
|
||||
// // value: "abc"
|
||||
//
|
||||
// # Monoid Operations
|
||||
//
|
||||
// The package provides monoid instances for combining validations:
|
||||
//
|
||||
// // Combine validation results
|
||||
// m := validation.ApplicativeMonoid(stringMonoid)
|
||||
// combined := m.Concat(validation.Success("hello"), validation.Success(" world"))
|
||||
// // Result: Success("hello world")
|
||||
//
|
||||
// # Integration
|
||||
//
|
||||
// This package integrates with:
|
||||
// - either: Validation is built on Either for error handling
|
||||
// - array: For collecting multiple errors
|
||||
// - monoid: For combining validation results
|
||||
// - reader: For context-dependent validation operations
|
||||
package validation
|
||||
112
v2/optics/codec/validation/monad.go
Normal file
112
v2/optics/codec/validation/monad.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/internal/applicative"
|
||||
)
|
||||
|
||||
// Of creates a successful validation result containing the given value.
|
||||
// This is the pure/return operation for the Validation monad.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// valid := Of(42) // Validation[int] containing 42
|
||||
func Of[A any](a A) Validation[A] {
|
||||
return either.Of[Errors](a)
|
||||
}
|
||||
|
||||
// Ap applies a validation containing a function to a validation containing a value.
|
||||
// This is the applicative apply operation that accumulates errors from both validations.
|
||||
// If either validation fails, all errors are collected. If both succeed, the function is applied.
|
||||
//
|
||||
// This enables combining multiple validations while collecting all errors:
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Validate multiple fields and collect all errors
|
||||
// validateUser := Ap(Ap(Of(func(name string) func(age int) User {
|
||||
// return func(age int) User { return User{name, age} }
|
||||
// }))(validateName))(validateAge)
|
||||
func Ap[B, A any](fa Validation[A]) Operator[func(A) B, B] {
|
||||
return either.ApV[B, A](ErrorsMonoid())(fa)
|
||||
}
|
||||
|
||||
// Map transforms the value inside a successful validation using the provided function.
|
||||
// If the validation is a failure, the errors are preserved unchanged.
|
||||
// This is the functor map operation for Validation.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// doubled := Map(func(x int) int { return x * 2 })(Of(21))
|
||||
// // Result: Success(42)
|
||||
func Map[A, B any](f func(A) B) Operator[A, B] {
|
||||
return either.Map[Errors](f)
|
||||
}
|
||||
|
||||
// Applicative creates an Applicative instance for Validation with error accumulation.
|
||||
//
|
||||
// This returns a lawful Applicative that accumulates validation errors using the Errors monoid.
|
||||
// Unlike the standard Either applicative which fails fast, this validation applicative collects
|
||||
// all errors when combining independent validations with Ap.
|
||||
//
|
||||
// The returned instance satisfies all applicative laws:
|
||||
// - Identity: Ap(Of(identity))(v) == v
|
||||
// - Homomorphism: Ap(Of(f))(Of(x)) == Of(f(x))
|
||||
// - Interchange: Ap(Of(f))(u) == Ap(Map(f => f(y))(u))(Of(y))
|
||||
// - Composition: Ap(Ap(Map(compose)(f))(g))(x) == Ap(f)(Ap(g)(x))
|
||||
//
|
||||
// Key behaviors:
|
||||
// - Of: lifts a value into a successful Validation (Right)
|
||||
// - Map: transforms successful values, preserves failures (standard functor)
|
||||
// - Ap: when both operands fail, combines all errors using the Errors monoid
|
||||
//
|
||||
// This is particularly useful for form validation, configuration validation, and any scenario
|
||||
// where you want to collect all validation errors at once rather than stopping at the first failure.
|
||||
//
|
||||
// Example - Validating Multiple Fields:
|
||||
//
|
||||
// app := Applicative[string, User]()
|
||||
//
|
||||
// // Validate individual fields
|
||||
// validateName := func(name string) Validation[string] {
|
||||
// if len(name) < 3 {
|
||||
// return Failure("Name must be at least 3 characters")
|
||||
// }
|
||||
// return Success(name)
|
||||
// }
|
||||
//
|
||||
// validateAge := func(age int) Validation[int] {
|
||||
// if age < 18 {
|
||||
// return Failure("Must be 18 or older")
|
||||
// }
|
||||
// return Success(age)
|
||||
// }
|
||||
//
|
||||
// // Create a curried constructor
|
||||
// makeUser := func(name string) func(int) User {
|
||||
// return func(age int) User {
|
||||
// return User{Name: name, Age: age}
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Combine validations - all errors are collected
|
||||
// name := validateName("ab") // Failure: name too short
|
||||
// age := validateAge(16) // Failure: age too low
|
||||
//
|
||||
// result := app.Ap(age)(app.Ap(name)(app.Of(makeUser)))
|
||||
// // result contains both validation errors:
|
||||
// // - "Name must be at least 3 characters"
|
||||
// // - "Must be 18 or older"
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The input value type (Right value)
|
||||
// - B: The output value type after transformation
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// An Applicative instance with Of, Map, and Ap operations that accumulate errors
|
||||
func Applicative[A, B any]() applicative.Applicative[A, B, Validation[A], Validation[B], Validation[func(A) B]] {
|
||||
return either.ApplicativeV[Errors, A, B](
|
||||
ErrorsMonoid(),
|
||||
)
|
||||
}
|
||||
921
v2/optics/codec/validation/monad_test.go
Normal file
921
v2/optics/codec/validation/monad_test.go
Normal file
@@ -0,0 +1,921 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOf(t *testing.T) {
|
||||
t.Run("creates successful validation", func(t *testing.T) {
|
||||
result := Of(42)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
strResult := Of("hello")
|
||||
assert.True(t, either.IsRight(strResult))
|
||||
|
||||
boolResult := Of(true)
|
||||
assert.True(t, either.IsRight(boolResult))
|
||||
|
||||
type Custom struct{ Value int }
|
||||
customResult := Of(Custom{Value: 100})
|
||||
assert.True(t, either.IsRight(customResult))
|
||||
})
|
||||
|
||||
t.Run("is equivalent to Success", func(t *testing.T) {
|
||||
value := 42
|
||||
ofResult := Of(value)
|
||||
successResult := Success(value)
|
||||
|
||||
assert.Equal(t, ofResult, successResult)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
t.Run("transforms successful validation", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
result := Map(double)(Of(21))
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
errs := Errors{&ValidationError{Messsage: "error"}}
|
||||
failure := Failures[int](errs)
|
||||
|
||||
double := func(x int) int { return x * 2 }
|
||||
result := Map(double)(failure)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("chains multiple maps", func(t *testing.T) {
|
||||
add10 := func(x int) int { return x + 10 }
|
||||
double := func(x int) int { return x * 2 }
|
||||
toString := func(x int) string { return fmt.Sprintf("%d", x) }
|
||||
|
||||
result := F.Pipe3(
|
||||
Of(5),
|
||||
Map(add10),
|
||||
Map(double),
|
||||
Map(toString),
|
||||
)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "30", value)
|
||||
})
|
||||
|
||||
t.Run("type transformation", func(t *testing.T) {
|
||||
length := func(s string) int { return len(s) }
|
||||
result := Map(length)(Of("hello"))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 5, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAp(t *testing.T) {
|
||||
t.Run("applies function to value when both succeed", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
funcValidation := Of(double)
|
||||
valueValidation := Of(21)
|
||||
|
||||
result := Ap[int, int](valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("accumulates errors when value fails", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
funcValidation := Of(double)
|
||||
valueValidation := Failures[int](Errors{
|
||||
&ValidationError{Messsage: "value error"},
|
||||
})
|
||||
|
||||
result := Ap[int, int](valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "value error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("accumulates errors when function fails", func(t *testing.T) {
|
||||
funcValidation := Failures[func(int) int](Errors{
|
||||
&ValidationError{Messsage: "function error"},
|
||||
})
|
||||
valueValidation := Of(21)
|
||||
|
||||
result := Ap[int, int](valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "function error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("accumulates all errors when both fail", func(t *testing.T) {
|
||||
funcValidation := Failures[func(int) int](Errors{
|
||||
&ValidationError{Messsage: "function error"},
|
||||
})
|
||||
valueValidation := Failures[int](Errors{
|
||||
&ValidationError{Messsage: "value error"},
|
||||
})
|
||||
|
||||
result := Ap[int, int](valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2)
|
||||
messages := []string{errors[0].Messsage, errors[1].Messsage}
|
||||
assert.Contains(t, messages, "function error")
|
||||
assert.Contains(t, messages, "value error")
|
||||
})
|
||||
|
||||
t.Run("applies with string transformation", func(t *testing.T) {
|
||||
toUpper := func(s string) string { return fmt.Sprintf("UPPER:%s", s) }
|
||||
funcValidation := Of(toUpper)
|
||||
valueValidation := Of("hello")
|
||||
|
||||
result := Ap[string, string](valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "UPPER:hello", value)
|
||||
})
|
||||
|
||||
t.Run("accumulates multiple validation errors from different sources", func(t *testing.T) {
|
||||
funcValidation := Failures[func(int) int](Errors{
|
||||
&ValidationError{Messsage: "function error 1"},
|
||||
&ValidationError{Messsage: "function error 2"},
|
||||
})
|
||||
valueValidation := Failures[int](Errors{
|
||||
&ValidationError{Messsage: "value error 1"},
|
||||
})
|
||||
|
||||
result := Ap[int, int](valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 3)
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "function error 1")
|
||||
assert.Contains(t, messages, "function error 2")
|
||||
assert.Contains(t, messages, "value error 1")
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadLaws(t *testing.T) {
|
||||
t.Run("functor identity law", func(t *testing.T) {
|
||||
// Map(id) == id
|
||||
value := Of(42)
|
||||
mapped := Map(F.Identity[int])(value)
|
||||
|
||||
assert.Equal(t, value, mapped)
|
||||
})
|
||||
|
||||
t.Run("functor composition law", func(t *testing.T) {
|
||||
// Map(f . g) == Map(f) . Map(g)
|
||||
f := func(x int) int { return x * 2 }
|
||||
g := func(x int) int { return x + 10 }
|
||||
composed := func(x int) int { return f(g(x)) }
|
||||
|
||||
value := Of(5)
|
||||
left := Map(composed)(value)
|
||||
right := F.Pipe2(value, Map(g), Map(f))
|
||||
|
||||
assert.Equal(t, left, right)
|
||||
})
|
||||
|
||||
t.Run("applicative identity law", func(t *testing.T) {
|
||||
// Ap(v)(Of(id)) == v
|
||||
v := Of(42)
|
||||
result := Ap[int, int](v)(Of(F.Identity[int]))
|
||||
|
||||
assert.Equal(t, v, result)
|
||||
})
|
||||
|
||||
t.Run("applicative homomorphism law", func(t *testing.T) {
|
||||
// Ap(Of(x))(Of(f)) == Of(f(x))
|
||||
f := func(x int) int { return x * 2 }
|
||||
x := 21
|
||||
|
||||
left := Ap[int, int](Of(x))(Of(f))
|
||||
right := Of(f(x))
|
||||
|
||||
assert.Equal(t, left, right)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMapWithOperator(t *testing.T) {
|
||||
t.Run("Map returns an Operator", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
operator := Map(double)
|
||||
|
||||
// Operator can be applied to different validations
|
||||
result1 := operator(Of(10))
|
||||
result2 := operator(Of(20))
|
||||
|
||||
val1 := either.MonadFold(result1,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
val2 := either.MonadFold(result2,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
assert.Equal(t, 20, val1)
|
||||
assert.Equal(t, 40, val2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApWithOperator(t *testing.T) {
|
||||
t.Run("Ap returns an Operator", func(t *testing.T) {
|
||||
valueValidation := Of(21)
|
||||
operator := Ap[int, int](valueValidation)
|
||||
|
||||
// Operator can be applied to different function validations
|
||||
double := func(x int) int { return x * 2 }
|
||||
triple := func(x int) int { return x * 3 }
|
||||
|
||||
result1 := operator(Of(double))
|
||||
result2 := operator(Of(triple))
|
||||
|
||||
val1 := either.MonadFold(result1,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
val2 := either.MonadFold(result2,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
assert.Equal(t, 42, val1)
|
||||
assert.Equal(t, 63, val2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicative(t *testing.T) {
|
||||
t.Run("returns non-nil instance", func(t *testing.T) {
|
||||
app := Applicative[int, string]()
|
||||
assert.NotNil(t, app)
|
||||
})
|
||||
|
||||
t.Run("multiple calls return independent instances", func(t *testing.T) {
|
||||
app1 := Applicative[int, string]()
|
||||
app2 := Applicative[int, string]()
|
||||
|
||||
// Both should work independently
|
||||
result1 := app1.Of(42)
|
||||
result2 := app2.Of(43)
|
||||
|
||||
assert.True(t, either.IsRight(result1))
|
||||
assert.True(t, either.IsRight(result2))
|
||||
|
||||
val1 := either.MonadFold(result1, func(Errors) int { return 0 }, F.Identity[int])
|
||||
val2 := either.MonadFold(result2, func(Errors) int { return 0 }, F.Identity[int])
|
||||
|
||||
assert.Equal(t, 42, val1)
|
||||
assert.Equal(t, 43, val2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeOf(t *testing.T) {
|
||||
app := Applicative[int, string]()
|
||||
|
||||
t.Run("wraps a value in Validation context", func(t *testing.T) {
|
||||
result := app.Of(42)
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("wraps string value", func(t *testing.T) {
|
||||
app := Applicative[string, int]()
|
||||
result := app.Of("hello")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "hello", value)
|
||||
})
|
||||
|
||||
t.Run("wraps zero value", func(t *testing.T) {
|
||||
result := app.Of(0)
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return -1 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 0, value)
|
||||
})
|
||||
|
||||
t.Run("wraps complex types", func(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
app := Applicative[User, string]()
|
||||
user := User{Name: "Alice", Age: 30}
|
||||
result := app.Of(user)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) User { return User{} },
|
||||
F.Identity[User],
|
||||
)
|
||||
assert.Equal(t, user, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeMap(t *testing.T) {
|
||||
app := Applicative[int, int]()
|
||||
|
||||
t.Run("maps a function over successful validation", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
result := app.Map(double)(app.Of(21))
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("maps type conversion", func(t *testing.T) {
|
||||
app := Applicative[int, string]()
|
||||
toString := func(x int) string { return fmt.Sprintf("%d", x) }
|
||||
result := app.Map(toString)(app.Of(42))
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "42", value)
|
||||
})
|
||||
|
||||
t.Run("maps identity function", func(t *testing.T) {
|
||||
result := app.Map(F.Identity[int])(app.Of(42))
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
errs := Errors{&ValidationError{Messsage: "error"}}
|
||||
failure := Failures[int](errs)
|
||||
|
||||
double := func(x int) int { return x * 2 }
|
||||
result := app.Map(double)(failure)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("chains multiple maps", func(t *testing.T) {
|
||||
app := Applicative[int, string]()
|
||||
add10 := func(x int) int { return x + 10 }
|
||||
double := func(x int) int { return x * 2 }
|
||||
toString := func(x int) string { return fmt.Sprintf("%d", x) }
|
||||
|
||||
result := F.Pipe3(
|
||||
app.Of(5),
|
||||
Map(add10),
|
||||
Map(double),
|
||||
app.Map(toString),
|
||||
)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "30", value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeAp(t *testing.T) {
|
||||
app := Applicative[int, int]()
|
||||
|
||||
t.Run("applies wrapped function to wrapped value when both succeed", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
funcValidation := Of(double)
|
||||
valueValidation := app.Of(21)
|
||||
|
||||
result := app.Ap(valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("accumulates errors when value fails", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
funcValidation := Of(double)
|
||||
valueValidation := Failures[int](Errors{
|
||||
&ValidationError{Messsage: "value error"},
|
||||
})
|
||||
|
||||
result := app.Ap(valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "value error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("accumulates errors when function fails", func(t *testing.T) {
|
||||
funcValidation := Failures[func(int) int](Errors{
|
||||
&ValidationError{Messsage: "function error"},
|
||||
})
|
||||
valueValidation := app.Of(21)
|
||||
|
||||
result := app.Ap(valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "function error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("accumulates all errors when both fail", func(t *testing.T) {
|
||||
funcValidation := Failures[func(int) int](Errors{
|
||||
&ValidationError{Messsage: "function error"},
|
||||
})
|
||||
valueValidation := Failures[int](Errors{
|
||||
&ValidationError{Messsage: "value error"},
|
||||
})
|
||||
|
||||
result := app.Ap(valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2)
|
||||
messages := []string{errors[0].Messsage, errors[1].Messsage}
|
||||
assert.Contains(t, messages, "function error")
|
||||
assert.Contains(t, messages, "value error")
|
||||
})
|
||||
|
||||
t.Run("applies with type conversion", func(t *testing.T) {
|
||||
app := Applicative[int, string]()
|
||||
toString := func(x int) string { return fmt.Sprintf("value:%d", x) }
|
||||
funcValidation := Of(toString)
|
||||
valueValidation := app.Of(42)
|
||||
|
||||
result := app.Ap(valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "value:42", value)
|
||||
})
|
||||
|
||||
t.Run("accumulates multiple errors from different sources", func(t *testing.T) {
|
||||
funcValidation := Failures[func(int) int](Errors{
|
||||
&ValidationError{Messsage: "function error 1"},
|
||||
&ValidationError{Messsage: "function error 2"},
|
||||
})
|
||||
valueValidation := Failures[int](Errors{
|
||||
&ValidationError{Messsage: "value error 1"},
|
||||
&ValidationError{Messsage: "value error 2"},
|
||||
})
|
||||
|
||||
result := app.Ap(valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 4)
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "function error 1")
|
||||
assert.Contains(t, messages, "function error 2")
|
||||
assert.Contains(t, messages, "value error 1")
|
||||
assert.Contains(t, messages, "value error 2")
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeComposition(t *testing.T) {
|
||||
app := Applicative[int, int]()
|
||||
|
||||
t.Run("composes Map and Of", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
result := F.Pipe1(
|
||||
app.Of(21),
|
||||
app.Map(double),
|
||||
)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("composes multiple Map operations", func(t *testing.T) {
|
||||
app := Applicative[int, string]()
|
||||
double := func(x int) int { return x * 2 }
|
||||
toString := func(x int) string { return fmt.Sprintf("%d", x) }
|
||||
|
||||
result := F.Pipe2(
|
||||
app.Of(21),
|
||||
Map(double),
|
||||
app.Map(toString),
|
||||
)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "42", value)
|
||||
})
|
||||
|
||||
t.Run("composes Map and Ap", func(t *testing.T) {
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
|
||||
ioFunc := F.Pipe1(
|
||||
app.Of(5),
|
||||
Map(add),
|
||||
)
|
||||
valueValidation := app.Of(16)
|
||||
|
||||
result := app.Ap(valueValidation)(ioFunc)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 21, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeLawsWithInstance(t *testing.T) {
|
||||
app := Applicative[int, int]()
|
||||
|
||||
t.Run("identity law: Ap(Of(id))(v) == v", func(t *testing.T) {
|
||||
identity := func(x int) int { return x }
|
||||
v := app.Of(42)
|
||||
|
||||
left := app.Ap(v)(Of(identity))
|
||||
right := v
|
||||
|
||||
assert.Equal(t, right, left)
|
||||
})
|
||||
|
||||
t.Run("homomorphism law: Ap(Of(x))(Of(f)) == Of(f(x))", func(t *testing.T) {
|
||||
f := func(x int) int { return x * 2 }
|
||||
x := 21
|
||||
|
||||
left := app.Ap(app.Of(x))(Of(f))
|
||||
right := app.Of(f(x))
|
||||
|
||||
assert.Equal(t, right, left)
|
||||
})
|
||||
|
||||
t.Run("interchange law: Ap(Of(y))(u) == Ap(u)(Of($ y))", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
u := Of(double)
|
||||
y := 21
|
||||
|
||||
left := app.Ap(app.Of(y))(u)
|
||||
|
||||
applyY := func(f func(int) int) int { return f(y) }
|
||||
right := Ap[int](u)(Of(applyY))
|
||||
|
||||
assert.Equal(t, right, left)
|
||||
})
|
||||
|
||||
t.Run("identity law with failure", func(t *testing.T) {
|
||||
identity := func(x int) int { return x }
|
||||
v := Failures[int](Errors{&ValidationError{Messsage: "error"}})
|
||||
|
||||
left := app.Ap(v)(Of(identity))
|
||||
right := v
|
||||
|
||||
assert.Equal(t, right, left)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeMultipleArguments(t *testing.T) {
|
||||
app := Applicative[int, int]()
|
||||
|
||||
t.Run("applies curried two-argument function", func(t *testing.T) {
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
|
||||
// Create validation with curried function
|
||||
funcValidation := F.Pipe1(
|
||||
app.Of(10),
|
||||
Map(add),
|
||||
)
|
||||
|
||||
// Apply to second argument
|
||||
result := app.Ap(app.Of(32))(funcValidation)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("applies curried three-argument function", func(t *testing.T) {
|
||||
add3 := func(a int) func(int) func(int) int {
|
||||
return func(b int) func(int) int {
|
||||
return func(c int) int {
|
||||
return a + b + c
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build up the computation step by step
|
||||
funcValidation1 := F.Pipe1(
|
||||
app.Of(10),
|
||||
Map(add3),
|
||||
)
|
||||
|
||||
funcValidation2 := Ap[func(int) int](app.Of(20))(funcValidation1)
|
||||
result := Ap[int](app.Of(12))(funcValidation2)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("accumulates errors from multiple arguments", func(t *testing.T) {
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
|
||||
// First argument fails
|
||||
arg1 := Failures[int](Errors{&ValidationError{Messsage: "arg1 error"}})
|
||||
// Second argument fails
|
||||
arg2 := Failures[int](Errors{&ValidationError{Messsage: "arg2 error"}})
|
||||
|
||||
funcValidation := F.Pipe1(arg1, Map(add))
|
||||
result := app.Ap(arg2)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2)
|
||||
messages := []string{errors[0].Messsage, errors[1].Messsage}
|
||||
assert.Contains(t, messages, "arg1 error")
|
||||
assert.Contains(t, messages, "arg2 error")
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeWithDifferentTypes(t *testing.T) {
|
||||
t.Run("int to string", func(t *testing.T) {
|
||||
app := Applicative[int, string]()
|
||||
toString := func(x int) string { return fmt.Sprintf("%d", x) }
|
||||
result := app.Map(toString)(app.Of(42))
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "42", value)
|
||||
})
|
||||
|
||||
t.Run("string to int", func(t *testing.T) {
|
||||
app := Applicative[string, int]()
|
||||
toLength := func(s string) int { return len(s) }
|
||||
result := app.Map(toLength)(app.Of("hello"))
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 5, value)
|
||||
})
|
||||
|
||||
t.Run("bool to string", func(t *testing.T) {
|
||||
app := Applicative[bool, string]()
|
||||
toString := func(b bool) string {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
result := app.Map(toString)(app.Of(true))
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "true", value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeRealWorldScenario(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
t.Run("validates user with all valid fields", func(t *testing.T) {
|
||||
validateName := func(name string) Validation[string] {
|
||||
if len(name) < 3 {
|
||||
return Failures[string](Errors{&ValidationError{Messsage: "Name must be at least 3 characters"}})
|
||||
}
|
||||
return Success(name)
|
||||
}
|
||||
|
||||
validateAge := func(age int) Validation[int] {
|
||||
if age < 18 {
|
||||
return Failures[int](Errors{&ValidationError{Messsage: "Must be 18 or older"}})
|
||||
}
|
||||
return Success(age)
|
||||
}
|
||||
|
||||
validateEmail := func(email string) Validation[string] {
|
||||
if len(email) == 0 {
|
||||
return Failures[string](Errors{&ValidationError{Messsage: "Email is required"}})
|
||||
}
|
||||
return Success(email)
|
||||
}
|
||||
|
||||
makeUser := func(name string) func(int) func(string) User {
|
||||
return func(age int) func(string) User {
|
||||
return func(email string) User {
|
||||
return User{Name: name, Age: age, Email: email}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
name := validateName("Alice")
|
||||
age := validateAge(25)
|
||||
email := validateEmail("alice@example.com")
|
||||
|
||||
// Use the standalone Ap function with proper type parameters
|
||||
result := Ap[User](email)(Ap[func(string) User](age)(Ap[func(int) func(string) User](name)(Of(makeUser))))
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
user := either.MonadFold(result,
|
||||
func(Errors) User { return User{} },
|
||||
F.Identity[User],
|
||||
)
|
||||
assert.Equal(t, "Alice", user.Name)
|
||||
assert.Equal(t, 25, user.Age)
|
||||
assert.Equal(t, "alice@example.com", user.Email)
|
||||
})
|
||||
|
||||
t.Run("accumulates all validation errors", func(t *testing.T) {
|
||||
validateName := func(name string) Validation[string] {
|
||||
if len(name) < 3 {
|
||||
return Failures[string](Errors{&ValidationError{Messsage: "Name must be at least 3 characters"}})
|
||||
}
|
||||
return Success(name)
|
||||
}
|
||||
|
||||
validateAge := func(age int) Validation[int] {
|
||||
if age < 18 {
|
||||
return Failures[int](Errors{&ValidationError{Messsage: "Must be 18 or older"}})
|
||||
}
|
||||
return Success(age)
|
||||
}
|
||||
|
||||
validateEmail := func(email string) Validation[string] {
|
||||
if len(email) == 0 {
|
||||
return Failures[string](Errors{&ValidationError{Messsage: "Email is required"}})
|
||||
}
|
||||
return Success(email)
|
||||
}
|
||||
|
||||
makeUser := func(name string) func(int) func(string) User {
|
||||
return func(age int) func(string) User {
|
||||
return func(email string) User {
|
||||
return User{Name: name, Age: age, Email: email}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All validations fail
|
||||
name := validateName("ab")
|
||||
age := validateAge(16)
|
||||
email := validateEmail("")
|
||||
|
||||
// Use the standalone Ap function with proper type parameters
|
||||
result := Ap[User](email)(Ap[func(string) User](age)(Ap[func(int) func(string) User](name)(Of(makeUser))))
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(User) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 3)
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "Name must be at least 3 characters")
|
||||
assert.Contains(t, messages, "Must be 18 or older")
|
||||
assert.Contains(t, messages, "Email is required")
|
||||
})
|
||||
}
|
||||
54
v2/optics/codec/validation/monoid.go
Normal file
54
v2/optics/codec/validation/monoid.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
)
|
||||
|
||||
// ErrorsMonoid returns a Monoid instance for Errors (array of ValidationError pointers).
|
||||
// The monoid concatenates error arrays, with an empty array as the identity element.
|
||||
// This is used internally by the applicative operations to accumulate validation errors.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// m := ErrorsMonoid()
|
||||
// combined := m.Concat(errors1, errors2) // Concatenates both error arrays
|
||||
// empty := m.Empty() // Returns empty error array
|
||||
func ErrorsMonoid() Monoid[Errors] {
|
||||
return A.Monoid[*ValidationError]()
|
||||
}
|
||||
|
||||
// ApplicativeMonoid creates a Monoid instance for Validation[A] given a Monoid for A.
|
||||
// This allows combining validation results where the success values are also combined
|
||||
// using the provided monoid. If any validation fails, all errors are accumulated.
|
||||
//
|
||||
// The resulting monoid:
|
||||
// - Empty: Returns a successful validation with the empty value from the inner monoid
|
||||
// - Concat: Combines two validations:
|
||||
// - Both success: Combines values using the inner monoid
|
||||
// - Any failure: Accumulates all errors
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import "github.com/IBM/fp-go/v2/string"
|
||||
//
|
||||
// // Create a monoid for validations of strings
|
||||
// m := ApplicativeMonoid(string.Monoid)
|
||||
//
|
||||
// v1 := Success("Hello")
|
||||
// v2 := Success(" World")
|
||||
// combined := m.Concat(v1, v2) // Success("Hello World")
|
||||
//
|
||||
// v3 := Failures[string](someErrors)
|
||||
// failed := m.Concat(v1, v3) // Failures with accumulated errors
|
||||
func ApplicativeMonoid[A any](m Monoid[A]) Monoid[Validation[A]] {
|
||||
|
||||
return M.ApplicativeMonoid(
|
||||
Of,
|
||||
either.MonadMap,
|
||||
either.MonadApV[A, A](ErrorsMonoid()),
|
||||
|
||||
m,
|
||||
)
|
||||
}
|
||||
353
v2/optics/codec/validation/monoid_test.go
Normal file
353
v2/optics/codec/validation/monoid_test.go
Normal file
@@ -0,0 +1,353 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
MO "github.com/IBM/fp-go/v2/monoid"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestErrorsMonoid(t *testing.T) {
|
||||
m := ErrorsMonoid()
|
||||
|
||||
t.Run("empty returns empty array", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
assert.NotNil(t, empty)
|
||||
assert.Len(t, empty, 0)
|
||||
})
|
||||
|
||||
t.Run("concat combines error arrays", func(t *testing.T) {
|
||||
errs1 := Errors{
|
||||
&ValidationError{Messsage: "error 1"},
|
||||
&ValidationError{Messsage: "error 2"},
|
||||
}
|
||||
errs2 := Errors{
|
||||
&ValidationError{Messsage: "error 3"},
|
||||
}
|
||||
|
||||
result := m.Concat(errs1, errs2)
|
||||
|
||||
assert.Len(t, result, 3)
|
||||
assert.Equal(t, "error 1", result[0].Messsage)
|
||||
assert.Equal(t, "error 2", result[1].Messsage)
|
||||
assert.Equal(t, "error 3", result[2].Messsage)
|
||||
})
|
||||
|
||||
t.Run("concat with empty preserves errors", func(t *testing.T) {
|
||||
errs := Errors{
|
||||
&ValidationError{Messsage: "error"},
|
||||
}
|
||||
empty := m.Empty()
|
||||
|
||||
result1 := m.Concat(errs, empty)
|
||||
result2 := m.Concat(empty, errs)
|
||||
|
||||
assert.Equal(t, errs, result1)
|
||||
assert.Equal(t, errs, result2)
|
||||
})
|
||||
|
||||
t.Run("concat is associative", func(t *testing.T) {
|
||||
errs1 := Errors{&ValidationError{Messsage: "1"}}
|
||||
errs2 := Errors{&ValidationError{Messsage: "2"}}
|
||||
errs3 := Errors{&ValidationError{Messsage: "3"}}
|
||||
|
||||
// (a + b) + c
|
||||
left := m.Concat(m.Concat(errs1, errs2), errs3)
|
||||
// a + (b + c)
|
||||
right := m.Concat(errs1, m.Concat(errs2, errs3))
|
||||
|
||||
assert.Len(t, left, 3)
|
||||
assert.Len(t, right, 3)
|
||||
for i := 0; i < 3; i++ {
|
||||
assert.Equal(t, left[i].Messsage, right[i].Messsage)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeMonoid(t *testing.T) {
|
||||
t.Run("with string monoid", func(t *testing.T) {
|
||||
m := ApplicativeMonoid(S.Monoid)
|
||||
|
||||
t.Run("empty returns successful validation with empty string", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
|
||||
assert.True(t, either.IsRight(empty))
|
||||
value := either.MonadFold(empty,
|
||||
func(Errors) string { return "ERROR" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "", value)
|
||||
})
|
||||
|
||||
t.Run("concat combines successful validations", func(t *testing.T) {
|
||||
v1 := Success("Hello")
|
||||
v2 := Success(" World")
|
||||
|
||||
result := m.Concat(v1, v2)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "Hello World", value)
|
||||
})
|
||||
|
||||
t.Run("concat with failure returns failure", func(t *testing.T) {
|
||||
v1 := Success("Hello")
|
||||
v2 := Failures[string](Errors{
|
||||
&ValidationError{Messsage: "error"},
|
||||
})
|
||||
|
||||
result := m.Concat(v1, v2)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("concat accumulates all errors from both failures", func(t *testing.T) {
|
||||
v1 := Failures[string](Errors{
|
||||
&ValidationError{Messsage: "error 1"},
|
||||
})
|
||||
v2 := Failures[string](Errors{
|
||||
&ValidationError{Messsage: "error 2"},
|
||||
})
|
||||
|
||||
result := m.Concat(v1, v2)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2)
|
||||
messages := []string{errors[0].Messsage, errors[1].Messsage}
|
||||
assert.Contains(t, messages, "error 1")
|
||||
assert.Contains(t, messages, "error 2")
|
||||
})
|
||||
|
||||
t.Run("concat with empty preserves validation", func(t *testing.T) {
|
||||
v := Success("test")
|
||||
empty := m.Empty()
|
||||
|
||||
result1 := m.Concat(v, empty)
|
||||
result2 := m.Concat(empty, v)
|
||||
|
||||
val1 := either.MonadFold(result1,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
val2 := either.MonadFold(result2,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
assert.Equal(t, "test", val1)
|
||||
assert.Equal(t, "test", val2)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with int addition monoid", func(t *testing.T) {
|
||||
intMonoid := MO.MakeMonoid(
|
||||
func(a, b int) int { return a + b },
|
||||
0,
|
||||
)
|
||||
|
||||
m := ApplicativeMonoid(intMonoid)
|
||||
|
||||
t.Run("empty returns zero", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
|
||||
value := either.MonadFold(empty,
|
||||
func(Errors) int { return -1 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 0, value)
|
||||
})
|
||||
|
||||
t.Run("concat adds values", func(t *testing.T) {
|
||||
v1 := Success(10)
|
||||
v2 := Success(32)
|
||||
|
||||
result := m.Concat(v1, v2)
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("multiple concat operations", func(t *testing.T) {
|
||||
v1 := Success(1)
|
||||
v2 := Success(2)
|
||||
v3 := Success(3)
|
||||
v4 := Success(4)
|
||||
|
||||
result := m.Concat(m.Concat(m.Concat(v1, v2), v3), v4)
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 10, value)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonoidLaws(t *testing.T) {
|
||||
t.Run("ErrorsMonoid satisfies monoid laws", func(t *testing.T) {
|
||||
m := ErrorsMonoid()
|
||||
|
||||
errs1 := Errors{&ValidationError{Messsage: "1"}}
|
||||
errs2 := Errors{&ValidationError{Messsage: "2"}}
|
||||
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
// empty + a = a
|
||||
result := m.Concat(m.Empty(), errs1)
|
||||
assert.Equal(t, errs1, result)
|
||||
})
|
||||
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
// a + empty = a
|
||||
result := m.Concat(errs1, m.Empty())
|
||||
assert.Equal(t, errs1, result)
|
||||
})
|
||||
|
||||
t.Run("associativity", func(t *testing.T) {
|
||||
errs3 := Errors{&ValidationError{Messsage: "3"}}
|
||||
// (a + b) + c = a + (b + c)
|
||||
left := m.Concat(m.Concat(errs1, errs2), errs3)
|
||||
right := m.Concat(errs1, m.Concat(errs2, errs3))
|
||||
|
||||
assert.Len(t, left, 3)
|
||||
assert.Len(t, right, 3)
|
||||
for i := 0; i < 3; i++ {
|
||||
assert.Equal(t, left[i].Messsage, right[i].Messsage)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("ApplicativeMonoid satisfies monoid laws", func(t *testing.T) {
|
||||
m := ApplicativeMonoid(S.Monoid)
|
||||
|
||||
v1 := Success("a")
|
||||
v2 := Success("b")
|
||||
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
// empty + a = a
|
||||
result := m.Concat(m.Empty(), v1)
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "a", value)
|
||||
})
|
||||
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
// a + empty = a
|
||||
result := m.Concat(v1, m.Empty())
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "a", value)
|
||||
})
|
||||
|
||||
t.Run("associativity", func(t *testing.T) {
|
||||
v3 := Success("c")
|
||||
// (a + b) + c = a + (b + c)
|
||||
left := m.Concat(m.Concat(v1, v2), v3)
|
||||
right := m.Concat(v1, m.Concat(v2, v3))
|
||||
|
||||
leftVal := either.MonadFold(left,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
rightVal := either.MonadFold(right,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
assert.Equal(t, "abc", leftVal)
|
||||
assert.Equal(t, "abc", rightVal)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeMonoidWithFailures(t *testing.T) {
|
||||
m := ApplicativeMonoid(S.Monoid)
|
||||
|
||||
t.Run("failure propagates through concat", func(t *testing.T) {
|
||||
v1 := Success("a")
|
||||
v2 := Failures[string](Errors{&ValidationError{Messsage: "error"}})
|
||||
v3 := Success("c")
|
||||
|
||||
result := m.Concat(m.Concat(v1, v2), v3)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
})
|
||||
|
||||
t.Run("multiple failures accumulate", func(t *testing.T) {
|
||||
v1 := Failures[string](Errors{&ValidationError{Messsage: "error 1"}})
|
||||
v2 := Failures[string](Errors{&ValidationError{Messsage: "error 2"}})
|
||||
v3 := Failures[string](Errors{&ValidationError{Messsage: "error 3"}})
|
||||
|
||||
result := m.Concat(m.Concat(v1, v2), v3)
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 3)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeMonoidEdgeCases(t *testing.T) {
|
||||
t.Run("with custom struct monoid", func(t *testing.T) {
|
||||
type Counter struct{ Count int }
|
||||
|
||||
counterMonoid := MO.MakeMonoid(
|
||||
func(a, b Counter) Counter { return Counter{Count: a.Count + b.Count} },
|
||||
Counter{Count: 0},
|
||||
)
|
||||
|
||||
m := ApplicativeMonoid(counterMonoid)
|
||||
|
||||
v1 := Success(Counter{Count: 5})
|
||||
v2 := Success(Counter{Count: 10})
|
||||
|
||||
result := m.Concat(v1, v2)
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) Counter { return Counter{} },
|
||||
F.Identity[Counter],
|
||||
)
|
||||
assert.Equal(t, 15, value.Count)
|
||||
})
|
||||
|
||||
t.Run("empty concat empty", func(t *testing.T) {
|
||||
m := ApplicativeMonoid(S.Monoid)
|
||||
|
||||
result := m.Concat(m.Empty(), m.Empty())
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "ERROR" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "", value)
|
||||
})
|
||||
}
|
||||
49
v2/optics/codec/validation/types.go
Normal file
49
v2/optics/codec/validation/types.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
// Either represents a value that can be one of two types: Left (error) or Right (success).
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
|
||||
// ContextEntry represents a single entry in the validation context path.
|
||||
// It tracks the location and type information during nested validation.
|
||||
ContextEntry struct {
|
||||
Key string // The key or field name (for objects/maps)
|
||||
Type string // The expected type name
|
||||
Actual any // The actual value being validated
|
||||
}
|
||||
|
||||
// Context is a stack of ContextEntry values representing the path through
|
||||
// nested structures during validation. Used to provide detailed error messages.
|
||||
Context = []ContextEntry
|
||||
|
||||
// ValidationError represents a single validation failure with context.
|
||||
ValidationError struct {
|
||||
Value any // The value that failed validation
|
||||
Context Context // The path to the value in nested structures
|
||||
Messsage string // Human-readable error message
|
||||
Cause error
|
||||
}
|
||||
|
||||
// Errors is a collection of validation errors.
|
||||
Errors = []*ValidationError
|
||||
|
||||
// Validation represents the result of a validation operation.
|
||||
// Left contains validation errors, Right contains the successfully validated value.
|
||||
Validation[A any] = Either[Errors, A]
|
||||
|
||||
// Reader represents a computation that depends on an environment R and produces a value A.
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
|
||||
Kleisli[A, B any] = Reader[A, Validation[B]]
|
||||
|
||||
Operator[A, B any] = Kleisli[Validation[A], B]
|
||||
|
||||
Monoid[A any] = monoid.Monoid[A]
|
||||
)
|
||||
125
v2/optics/codec/validation/validation.go
Normal file
125
v2/optics/codec/validation/validation.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
)
|
||||
|
||||
// Error implements the error interface for ValidationError.
|
||||
// Returns a generic error message indicating this is a validation error.
|
||||
// For detailed error information, use String() or Format() methods.
|
||||
|
||||
// Error implements the error interface for ValidationError.
|
||||
// Returns a generic error message.
|
||||
func (v *ValidationError) Error() string {
|
||||
return "ValidationError"
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying cause error if present.
|
||||
// This allows ValidationError to work with errors.Is and errors.As.
|
||||
func (v *ValidationError) Unwrap() error {
|
||||
return v.Cause
|
||||
}
|
||||
|
||||
// String returns a simple string representation of the validation error.
|
||||
// Returns the error message prefixed with "ValidationError: ".
|
||||
func (v *ValidationError) String() string {
|
||||
return fmt.Sprintf("ValidationError: %s", v.Messsage)
|
||||
}
|
||||
|
||||
// Format implements fmt.Formatter for custom formatting of ValidationError.
|
||||
// It includes the context path, message, and optionally the cause error.
|
||||
// Supports verbs: %s, %v, %+v (with additional details)
|
||||
func (v *ValidationError) Format(s fmt.State, verb rune) {
|
||||
// Build the context path
|
||||
path := ""
|
||||
for i, entry := range v.Context {
|
||||
if i > 0 {
|
||||
path += "."
|
||||
}
|
||||
if entry.Key != "" {
|
||||
path += entry.Key
|
||||
} else {
|
||||
path += entry.Type
|
||||
}
|
||||
}
|
||||
|
||||
// Start with the path if available
|
||||
result := ""
|
||||
if path != "" {
|
||||
result = fmt.Sprintf("at %s: ", path)
|
||||
}
|
||||
|
||||
// Add the message
|
||||
result += v.Messsage
|
||||
|
||||
// Add the cause if present
|
||||
if v.Cause != nil {
|
||||
if s.Flag('+') && verb == 'v' {
|
||||
// Verbose format with detailed cause
|
||||
result += fmt.Sprintf("\n caused by: %+v", v.Cause)
|
||||
} else {
|
||||
result += fmt.Sprintf(" (caused by: %v)", v.Cause)
|
||||
}
|
||||
}
|
||||
|
||||
// Add value information for verbose format
|
||||
if s.Flag('+') && verb == 'v' {
|
||||
result += fmt.Sprintf("\n value: %#v", v.Value)
|
||||
}
|
||||
|
||||
fmt.Fprint(s, result)
|
||||
}
|
||||
|
||||
// Failures creates a validation failure from a collection of errors.
|
||||
// Returns a Left Either containing the errors.
|
||||
func Failures[T any](err Errors) Validation[T] {
|
||||
return either.Left[T](err)
|
||||
}
|
||||
|
||||
// FailureWithMessage creates a validation failure with a custom message.
|
||||
// Returns a Reader that takes a Context and produces a Validation[T] failure.
|
||||
// This is useful for creating context-aware validation errors.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// fail := FailureWithMessage[int]("abc", "expected integer")
|
||||
// result := fail([]ContextEntry{{Key: "age", Type: "int"}})
|
||||
func FailureWithMessage[T any](value any, message string) Reader[Context, Validation[T]] {
|
||||
return func(context Context) Validation[T] {
|
||||
return Failures[T](A.Of(&ValidationError{
|
||||
Value: value,
|
||||
Context: context,
|
||||
Messsage: message,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// FailureWithError creates a validation failure with a custom message and underlying cause.
|
||||
// Returns a Reader that takes an error, then a Context, and produces a Validation[T] failure.
|
||||
// This is useful for wrapping errors from other operations while maintaining validation context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// fail := FailureWithError[int]("abc", "parse failed")
|
||||
// result := fail(parseErr)([]ContextEntry{{Key: "count", Type: "int"}})
|
||||
func FailureWithError[T any](value any, message string) Reader[error, Reader[Context, Validation[T]]] {
|
||||
return func(err error) Reader[Context, Validation[T]] {
|
||||
return func(context Context) Validation[T] {
|
||||
return Failures[T](A.Of(&ValidationError{
|
||||
Value: value,
|
||||
Context: context,
|
||||
Messsage: message,
|
||||
Cause: err,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Success creates a successful validation result.
|
||||
// Returns a Right Either containing the validated value.
|
||||
func Success[T any](value T) Validation[T] {
|
||||
return either.Of[Errors](value)
|
||||
}
|
||||
419
v2/optics/codec/validation/validation_test.go
Normal file
419
v2/optics/codec/validation/validation_test.go
Normal file
@@ -0,0 +1,419 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidationError_Error(t *testing.T) {
|
||||
err := &ValidationError{
|
||||
Value: "test",
|
||||
Messsage: "invalid value",
|
||||
}
|
||||
|
||||
assert.Equal(t, "ValidationError", err.Error())
|
||||
}
|
||||
|
||||
func TestValidationError_String(t *testing.T) {
|
||||
err := &ValidationError{
|
||||
Value: "test",
|
||||
Messsage: "invalid value",
|
||||
}
|
||||
|
||||
expected := "ValidationError: invalid value"
|
||||
assert.Equal(t, expected, err.String())
|
||||
}
|
||||
|
||||
func TestValidationError_Unwrap(t *testing.T) {
|
||||
t.Run("with cause", func(t *testing.T) {
|
||||
cause := errors.New("underlying error")
|
||||
err := &ValidationError{
|
||||
Value: "test",
|
||||
Messsage: "invalid value",
|
||||
Cause: cause,
|
||||
}
|
||||
|
||||
assert.Equal(t, cause, err.Unwrap())
|
||||
})
|
||||
|
||||
t.Run("without cause", func(t *testing.T) {
|
||||
err := &ValidationError{
|
||||
Value: "test",
|
||||
Messsage: "invalid value",
|
||||
}
|
||||
|
||||
assert.Nil(t, err.Unwrap())
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidationError_Format(t *testing.T) {
|
||||
t.Run("simple format without context", func(t *testing.T) {
|
||||
err := &ValidationError{
|
||||
Value: "test",
|
||||
Messsage: "invalid value",
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("%v", err)
|
||||
assert.Equal(t, "invalid value", result)
|
||||
})
|
||||
|
||||
t.Run("with context path", func(t *testing.T) {
|
||||
err := &ValidationError{
|
||||
Value: "test",
|
||||
Context: []ContextEntry{{Key: "user"}, {Key: "name"}},
|
||||
Messsage: "must not be empty",
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("%v", err)
|
||||
assert.Equal(t, "at user.name: must not be empty", result)
|
||||
})
|
||||
|
||||
t.Run("with context using type", func(t *testing.T) {
|
||||
err := &ValidationError{
|
||||
Value: 123,
|
||||
Context: []ContextEntry{{Type: "User"}, {Key: "age"}},
|
||||
Messsage: "must be positive",
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("%v", err)
|
||||
assert.Equal(t, "at User.age: must be positive", result)
|
||||
})
|
||||
|
||||
t.Run("with cause - simple format", func(t *testing.T) {
|
||||
cause := errors.New("parse error")
|
||||
err := &ValidationError{
|
||||
Value: "abc",
|
||||
Messsage: "invalid number",
|
||||
Cause: cause,
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("%v", err)
|
||||
assert.Equal(t, "invalid number (caused by: parse error)", result)
|
||||
})
|
||||
|
||||
t.Run("with cause - verbose format", func(t *testing.T) {
|
||||
cause := errors.New("parse error")
|
||||
err := &ValidationError{
|
||||
Value: "abc",
|
||||
Messsage: "invalid number",
|
||||
Cause: cause,
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("%+v", err)
|
||||
assert.Contains(t, result, "invalid number")
|
||||
assert.Contains(t, result, "caused by: parse error")
|
||||
assert.Contains(t, result, `value: "abc"`)
|
||||
})
|
||||
|
||||
t.Run("verbose format shows value", func(t *testing.T) {
|
||||
err := &ValidationError{
|
||||
Value: 42,
|
||||
Messsage: "out of range",
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("%+v", err)
|
||||
assert.Contains(t, result, "out of range")
|
||||
assert.Contains(t, result, "value: 42")
|
||||
})
|
||||
|
||||
t.Run("complex context path", func(t *testing.T) {
|
||||
err := &ValidationError{
|
||||
Value: "invalid",
|
||||
Context: []ContextEntry{
|
||||
{Key: "user"},
|
||||
{Key: "address"},
|
||||
{Key: "zipCode"},
|
||||
},
|
||||
Messsage: "invalid format",
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("%v", err)
|
||||
assert.Equal(t, "at user.address.zipCode: invalid format", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFailures(t *testing.T) {
|
||||
t.Run("creates left either with errors", func(t *testing.T) {
|
||||
errs := Errors{
|
||||
&ValidationError{Value: "test", Messsage: "error 1"},
|
||||
&ValidationError{Value: "test", Messsage: "error 2"},
|
||||
}
|
||||
|
||||
result := Failures[int](errs)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
left := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, left, 2)
|
||||
assert.Equal(t, "error 1", left[0].Messsage)
|
||||
assert.Equal(t, "error 2", left[1].Messsage)
|
||||
})
|
||||
|
||||
t.Run("preserves error details", func(t *testing.T) {
|
||||
errs := Errors{
|
||||
&ValidationError{
|
||||
Value: "abc",
|
||||
Context: []ContextEntry{{Key: "field"}},
|
||||
Messsage: "invalid",
|
||||
Cause: errors.New("cause"),
|
||||
},
|
||||
}
|
||||
|
||||
result := Failures[string](errs)
|
||||
|
||||
left := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
require.Len(t, left, 1)
|
||||
assert.Equal(t, "abc", left[0].Value)
|
||||
assert.Equal(t, "invalid", left[0].Messsage)
|
||||
assert.NotNil(t, left[0].Cause)
|
||||
assert.Len(t, left[0].Context, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSuccess(t *testing.T) {
|
||||
t.Run("creates right either with value", func(t *testing.T) {
|
||||
result := Success(42)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
strResult := Success("hello")
|
||||
str := either.MonadFold(strResult,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "hello", str)
|
||||
|
||||
boolResult := Success(true)
|
||||
b := either.MonadFold(boolResult,
|
||||
func(Errors) bool { return false },
|
||||
F.Identity[bool],
|
||||
)
|
||||
assert.Equal(t, true, b)
|
||||
|
||||
type Custom struct{ Name string }
|
||||
customResult := Success(Custom{Name: "test"})
|
||||
custom := either.MonadFold(customResult,
|
||||
func(Errors) Custom { return Custom{} },
|
||||
F.Identity[Custom],
|
||||
)
|
||||
assert.Equal(t, "test", custom.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFailureWithMessage(t *testing.T) {
|
||||
t.Run("creates failure with context", func(t *testing.T) {
|
||||
fail := FailureWithMessage[int]("abc", "expected integer")
|
||||
context := []ContextEntry{{Key: "age", Type: "int"}}
|
||||
|
||||
result := fail(context)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errs := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
require.Len(t, errs, 1)
|
||||
assert.Equal(t, "abc", errs[0].Value)
|
||||
assert.Equal(t, "expected integer", errs[0].Messsage)
|
||||
assert.Equal(t, context, errs[0].Context)
|
||||
assert.Nil(t, errs[0].Cause)
|
||||
})
|
||||
|
||||
t.Run("works with empty context", func(t *testing.T) {
|
||||
fail := FailureWithMessage[string](123, "wrong type")
|
||||
result := fail(nil)
|
||||
|
||||
errs := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
require.Len(t, errs, 1)
|
||||
assert.Equal(t, 123, errs[0].Value)
|
||||
assert.Nil(t, errs[0].Context)
|
||||
})
|
||||
|
||||
t.Run("preserves complex context", func(t *testing.T) {
|
||||
fail := FailureWithMessage[bool]("not a bool", "type mismatch")
|
||||
context := []ContextEntry{
|
||||
{Key: "user"},
|
||||
{Key: "settings"},
|
||||
{Key: "enabled"},
|
||||
}
|
||||
|
||||
result := fail(context)
|
||||
|
||||
errs := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(bool) Errors { return nil },
|
||||
)
|
||||
require.Len(t, errs, 1)
|
||||
assert.Equal(t, context, errs[0].Context)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFailureWithError(t *testing.T) {
|
||||
t.Run("creates failure with cause and context", func(t *testing.T) {
|
||||
cause := errors.New("parse failed")
|
||||
fail := FailureWithError[int]("abc", "invalid number")
|
||||
context := []ContextEntry{{Key: "count"}}
|
||||
|
||||
result := fail(cause)(context)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errs := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
require.Len(t, errs, 1)
|
||||
assert.Equal(t, "abc", errs[0].Value)
|
||||
assert.Equal(t, "invalid number", errs[0].Messsage)
|
||||
assert.Equal(t, context, errs[0].Context)
|
||||
assert.Equal(t, cause, errs[0].Cause)
|
||||
})
|
||||
|
||||
t.Run("cause is unwrappable", func(t *testing.T) {
|
||||
cause := errors.New("underlying")
|
||||
fail := FailureWithError[string](nil, "wrapper")
|
||||
result := fail(cause)(nil)
|
||||
|
||||
errs := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
require.Len(t, errs, 1)
|
||||
assert.True(t, errors.Is(errs[0], cause))
|
||||
})
|
||||
|
||||
t.Run("works with complex error chain", func(t *testing.T) {
|
||||
root := errors.New("root cause")
|
||||
wrapped := fmt.Errorf("wrapped: %w", root)
|
||||
fail := FailureWithError[int](0, "validation failed")
|
||||
|
||||
result := fail(wrapped)([]ContextEntry{{Key: "field"}})
|
||||
|
||||
errs := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
require.Len(t, errs, 1)
|
||||
assert.True(t, errors.Is(errs[0], root))
|
||||
assert.True(t, errors.Is(errs[0], wrapped))
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidationIntegration(t *testing.T) {
|
||||
t.Run("success and failure can be combined", func(t *testing.T) {
|
||||
success := Success(42)
|
||||
failure := Failures[int](Errors{
|
||||
&ValidationError{Value: "bad", Messsage: "error"},
|
||||
})
|
||||
|
||||
assert.True(t, either.IsRight(success))
|
||||
assert.True(t, either.IsLeft(failure))
|
||||
})
|
||||
|
||||
t.Run("context provides meaningful error paths", func(t *testing.T) {
|
||||
fail := FailureWithMessage[string](nil, "required field")
|
||||
context := []ContextEntry{
|
||||
{Key: "request"},
|
||||
{Key: "body"},
|
||||
{Key: "user"},
|
||||
{Key: "email"},
|
||||
}
|
||||
|
||||
result := fail(context)
|
||||
errs := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
|
||||
formatted := fmt.Sprintf("%v", errs[0])
|
||||
assert.Contains(t, formatted, "request.body.user.email")
|
||||
assert.Contains(t, formatted, "required field")
|
||||
})
|
||||
|
||||
t.Run("multiple errors can be collected", func(t *testing.T) {
|
||||
errs := Errors{
|
||||
&ValidationError{
|
||||
Context: []ContextEntry{{Key: "name"}},
|
||||
Messsage: "too short",
|
||||
},
|
||||
&ValidationError{
|
||||
Context: []ContextEntry{{Key: "age"}},
|
||||
Messsage: "must be positive",
|
||||
},
|
||||
&ValidationError{
|
||||
Context: []ContextEntry{{Key: "email"}},
|
||||
Messsage: "invalid format",
|
||||
},
|
||||
}
|
||||
|
||||
result := Failures[any](errs)
|
||||
collected := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(any) Errors { return nil },
|
||||
)
|
||||
|
||||
assert.Len(t, collected, 3)
|
||||
messages := make([]string, len(collected))
|
||||
for i, err := range collected {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "too short")
|
||||
assert.Contains(t, messages, "must be positive")
|
||||
assert.Contains(t, messages, "invalid format")
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidationError_FormatEdgeCases(t *testing.T) {
|
||||
t.Run("empty message", func(t *testing.T) {
|
||||
err := &ValidationError{
|
||||
Value: "test",
|
||||
Messsage: "",
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("%v", err)
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
|
||||
t.Run("context with empty keys", func(t *testing.T) {
|
||||
err := &ValidationError{
|
||||
Value: "test",
|
||||
Context: []ContextEntry{{Key: ""}, {Type: "Type"}, {Key: ""}},
|
||||
Messsage: "error",
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("%v", err)
|
||||
// Should handle empty keys gracefully
|
||||
assert.Contains(t, result, "error")
|
||||
})
|
||||
|
||||
t.Run("nil value", func(t *testing.T) {
|
||||
err := &ValidationError{
|
||||
Value: nil,
|
||||
Messsage: "nil not allowed",
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("%+v", err)
|
||||
assert.Contains(t, result, "nil not allowed")
|
||||
assert.Contains(t, result, "value: <nil>")
|
||||
})
|
||||
}
|
||||
11
v2/optics/decoder/types.go
Normal file
11
v2/optics/decoder/types.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package decoder
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
type (
|
||||
Result[A any] = result.Result[A]
|
||||
|
||||
Decoder[I, A any] = result.Kleisli[I, A]
|
||||
)
|
||||
7
v2/optics/encoder/types.go
Normal file
7
v2/optics/encoder/types.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package encoder
|
||||
|
||||
import "github.com/IBM/fp-go/v2/reader"
|
||||
|
||||
type (
|
||||
Encoder[O, A any] = reader.Reader[A, O]
|
||||
)
|
||||
@@ -59,8 +59,5 @@ func Compose[
|
||||
return G.Compose[
|
||||
G.Traversal[A, B, HKTA, HKTB],
|
||||
G.Traversal[S, A, HKTS, HKTA],
|
||||
G.Traversal[S, B, HKTS, HKTB],
|
||||
S, A, B,
|
||||
HKTS, HKTA, HKTB,
|
||||
](ab)
|
||||
G.Traversal[S, B, HKTS, HKTB]](ab)
|
||||
}
|
||||
|
||||
@@ -475,3 +475,41 @@ func Flap[B, A any](a A) Operator[func(A) B, B] {
|
||||
return None[B]()
|
||||
}
|
||||
}
|
||||
|
||||
// Zero returns the zero value of an [Option], which is None.
|
||||
// This function is useful as an identity element in monoid operations or for creating an empty Option.
|
||||
//
|
||||
// The zero value for Option[A] is always None, representing the absence of a value.
|
||||
// This is consistent with the Option monad's semantics where None represents "no value"
|
||||
// and Some represents "a value".
|
||||
//
|
||||
// Important: Zero() returns the same value as the default initialization of Option[A].
|
||||
// When you declare `var o Option[A]` without initialization, it has the same value as Zero[A]().
|
||||
//
|
||||
// Note: Unlike other types where zero might be a default value, Option's zero is explicitly
|
||||
// the absence of any value (None), not Some with a zero value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Zero Option of any type is always None
|
||||
// o1 := option.Zero[int]() // None
|
||||
// o2 := option.Zero[string]() // None
|
||||
// o3 := option.Zero[*int]() // None
|
||||
//
|
||||
// // Zero equals default initialization
|
||||
// var defaultInit Option[int]
|
||||
// zero := option.Zero[int]()
|
||||
// assert.Equal(t, defaultInit, zero) // true
|
||||
//
|
||||
// // Verify it's None
|
||||
// o := option.Zero[int]()
|
||||
// assert.True(t, option.IsNone(o)) // true
|
||||
// assert.False(t, option.IsSome(o)) // false
|
||||
//
|
||||
// // Different from Some with zero value
|
||||
// someZero := option.Some(0) // Some(0)
|
||||
// zero := option.Zero[int]() // None
|
||||
// assert.NotEqual(t, someZero, zero) // they are different
|
||||
func Zero[A any]() Option[A] {
|
||||
return None[A]()
|
||||
}
|
||||
|
||||
@@ -174,3 +174,199 @@ func TestAlt(t *testing.T) {
|
||||
assert.Equal(t, Some(1), F.Pipe1(None[int](), Alt(F.Constant(Some(1)))))
|
||||
assert.Equal(t, None[int](), F.Pipe1(None[int](), Alt(F.Constant(None[int]()))))
|
||||
}
|
||||
|
||||
// TestZeroWithIntegers tests Zero function with integer types
|
||||
func TestZeroWithIntegers(t *testing.T) {
|
||||
o := Zero[int]()
|
||||
|
||||
assert.True(t, IsNone(o), "Zero should create a None value")
|
||||
assert.False(t, IsSome(o), "Zero should not create a Some value")
|
||||
}
|
||||
|
||||
// TestZeroWithStrings tests Zero function with string types
|
||||
func TestZeroWithStrings(t *testing.T) {
|
||||
o := Zero[string]()
|
||||
|
||||
assert.True(t, IsNone(o), "Zero should create a None value")
|
||||
assert.False(t, IsSome(o), "Zero should not create a Some value")
|
||||
}
|
||||
|
||||
// TestZeroWithBooleans tests Zero function with boolean types
|
||||
func TestZeroWithBooleans(t *testing.T) {
|
||||
o := Zero[bool]()
|
||||
|
||||
assert.True(t, IsNone(o), "Zero should create a None value")
|
||||
assert.False(t, IsSome(o), "Zero should not create a Some value")
|
||||
}
|
||||
|
||||
// TestZeroWithFloats tests Zero function with float types
|
||||
func TestZeroWithFloats(t *testing.T) {
|
||||
o := Zero[float64]()
|
||||
|
||||
assert.True(t, IsNone(o), "Zero should create a None value")
|
||||
assert.False(t, IsSome(o), "Zero should not create a Some value")
|
||||
}
|
||||
|
||||
// TestZeroWithPointers tests Zero function with pointer types
|
||||
func TestZeroWithPointers(t *testing.T) {
|
||||
o := Zero[*int]()
|
||||
|
||||
assert.True(t, IsNone(o), "Zero should create a None value")
|
||||
assert.False(t, IsSome(o), "Zero should not create a Some value")
|
||||
}
|
||||
|
||||
// TestZeroWithSlices tests Zero function with slice types
|
||||
func TestZeroWithSlices(t *testing.T) {
|
||||
o := Zero[[]int]()
|
||||
|
||||
assert.True(t, IsNone(o), "Zero should create a None value")
|
||||
assert.False(t, IsSome(o), "Zero should not create a Some value")
|
||||
}
|
||||
|
||||
// TestZeroWithMaps tests Zero function with map types
|
||||
func TestZeroWithMaps(t *testing.T) {
|
||||
o := Zero[map[string]int]()
|
||||
|
||||
assert.True(t, IsNone(o), "Zero should create a None value")
|
||||
assert.False(t, IsSome(o), "Zero should not create a Some value")
|
||||
}
|
||||
|
||||
// TestZeroWithStructs tests Zero function with struct types
|
||||
func TestZeroWithStructs(t *testing.T) {
|
||||
type TestStruct struct {
|
||||
Field1 int
|
||||
Field2 string
|
||||
}
|
||||
|
||||
o := Zero[TestStruct]()
|
||||
|
||||
assert.True(t, IsNone(o), "Zero should create a None value")
|
||||
assert.False(t, IsSome(o), "Zero should not create a Some value")
|
||||
}
|
||||
|
||||
// TestZeroWithInterfaces tests Zero function with interface types
|
||||
func TestZeroWithInterfaces(t *testing.T) {
|
||||
o := Zero[interface{}]()
|
||||
|
||||
assert.True(t, IsNone(o), "Zero should create a None value")
|
||||
assert.False(t, IsSome(o), "Zero should not create a Some value")
|
||||
}
|
||||
|
||||
// TestZeroIsNotSomeWithZeroValue tests that Zero is different from Some(zero value)
|
||||
func TestZeroIsNotSomeWithZeroValue(t *testing.T) {
|
||||
// Zero returns None
|
||||
zero := Zero[int]()
|
||||
assert.True(t, IsNone(zero), "Zero should be None")
|
||||
|
||||
// Some with zero value is different
|
||||
someZero := Some(0)
|
||||
assert.True(t, IsSome(someZero), "Some(0) should be Some")
|
||||
|
||||
// They are not equal
|
||||
assert.NotEqual(t, zero, someZero, "Zero (None) should not equal Some(0)")
|
||||
}
|
||||
|
||||
// TestZeroCanBeUsedWithOtherFunctions tests that Zero Options work with other option functions
|
||||
func TestZeroCanBeUsedWithOtherFunctions(t *testing.T) {
|
||||
o := Zero[int]()
|
||||
|
||||
// Test with Map - should remain None
|
||||
mapped := MonadMap(o, func(n int) string {
|
||||
return fmt.Sprintf("%d", n)
|
||||
})
|
||||
assert.True(t, IsNone(mapped), "Mapped Zero should still be None")
|
||||
|
||||
// Test with Chain - should remain None
|
||||
chained := MonadChain(o, func(n int) Option[string] {
|
||||
return Some(fmt.Sprintf("value: %d", n))
|
||||
})
|
||||
assert.True(t, IsNone(chained), "Chained Zero should still be None")
|
||||
|
||||
// Test with Fold - should use onNone branch
|
||||
folded := MonadFold(o,
|
||||
func() string { return "none" },
|
||||
func(n int) string { return fmt.Sprintf("some: %d", n) },
|
||||
)
|
||||
assert.Equal(t, "none", folded, "Folded Zero should use onNone branch")
|
||||
|
||||
// Test with GetOrElse
|
||||
value := GetOrElse(func() int { return 42 })(o)
|
||||
assert.Equal(t, 42, value, "GetOrElse on Zero should return default value")
|
||||
}
|
||||
|
||||
// TestZeroEquality tests that multiple Zero calls produce equal Options
|
||||
func TestZeroEquality(t *testing.T) {
|
||||
o1 := Zero[int]()
|
||||
o2 := Zero[int]()
|
||||
|
||||
assert.Equal(t, IsNone(o1), IsNone(o2), "Both should be None")
|
||||
assert.Equal(t, IsSome(o1), IsSome(o2), "Both should not be Some")
|
||||
assert.Equal(t, o1, o2, "Zero values should be equal")
|
||||
}
|
||||
|
||||
// TestZeroWithComplexTypes tests Zero with more complex nested types
|
||||
func TestZeroWithComplexTypes(t *testing.T) {
|
||||
type ComplexType struct {
|
||||
Nested map[string][]int
|
||||
Ptr *string
|
||||
}
|
||||
|
||||
o := Zero[ComplexType]()
|
||||
|
||||
assert.True(t, IsNone(o), "Zero should create a None value")
|
||||
assert.False(t, IsSome(o), "Zero should not create a Some value")
|
||||
}
|
||||
|
||||
// TestZeroWithNestedOption tests Zero with nested Option type
|
||||
func TestZeroWithNestedOption(t *testing.T) {
|
||||
o := Zero[Option[int]]()
|
||||
|
||||
assert.True(t, IsNone(o), "Zero should create a None value")
|
||||
assert.False(t, IsSome(o), "Zero should not create a Some value")
|
||||
}
|
||||
|
||||
// TestZeroIsAlwaysNone tests that Zero never creates a Some value
|
||||
func TestZeroIsAlwaysNone(t *testing.T) {
|
||||
// Test with various types
|
||||
o1 := Zero[int]()
|
||||
o2 := Zero[string]()
|
||||
o3 := Zero[bool]()
|
||||
o4 := Zero[*int]()
|
||||
o5 := Zero[[]string]()
|
||||
|
||||
assert.True(t, IsNone(o1), "Zero should always be None")
|
||||
assert.True(t, IsNone(o2), "Zero should always be None")
|
||||
assert.True(t, IsNone(o3), "Zero should always be None")
|
||||
assert.True(t, IsNone(o4), "Zero should always be None")
|
||||
assert.True(t, IsNone(o5), "Zero should always be None")
|
||||
|
||||
assert.False(t, IsSome(o1), "Zero should never be Some")
|
||||
assert.False(t, IsSome(o2), "Zero should never be Some")
|
||||
assert.False(t, IsSome(o3), "Zero should never be Some")
|
||||
assert.False(t, IsSome(o4), "Zero should never be Some")
|
||||
assert.False(t, IsSome(o5), "Zero should never be Some")
|
||||
}
|
||||
|
||||
// TestZeroEqualsNone tests that Zero is equivalent to None
|
||||
func TestZeroEqualsNone(t *testing.T) {
|
||||
zero := Zero[int]()
|
||||
none := None[int]()
|
||||
|
||||
assert.Equal(t, zero, none, "Zero should be equal to None")
|
||||
assert.Equal(t, IsNone(zero), IsNone(none), "Both should be None")
|
||||
assert.Equal(t, IsSome(zero), IsSome(none), "Both should not be Some")
|
||||
}
|
||||
|
||||
// TestZeroEqualsDefaultInitialization tests that Zero returns the same value as default initialization
|
||||
func TestZeroEqualsDefaultInitialization(t *testing.T) {
|
||||
// Default initialization of Option
|
||||
var defaultInit Option[int]
|
||||
|
||||
// Zero function
|
||||
zero := Zero[int]()
|
||||
|
||||
// They should be equal
|
||||
assert.Equal(t, defaultInit, zero, "Zero should equal default initialization")
|
||||
assert.Equal(t, IsNone(defaultInit), IsNone(zero), "Both should be None")
|
||||
assert.Equal(t, IsSome(defaultInit), IsSome(zero), "Both should not be Some")
|
||||
}
|
||||
|
||||
320
v2/ord/monoid_test.go
Normal file
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)
|
||||
}
|
||||
}
|
||||
@@ -171,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))
|
||||
@@ -373,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)
|
||||
}
|
||||
@@ -536,3 +536,49 @@ func Merge[F ~func(B) func(A) R, A, B, R any](f F) func(Pair[A, B]) R {
|
||||
return f(Tail(p))(Head(p))
|
||||
}
|
||||
}
|
||||
|
||||
// Zero returns the zero value of a [Pair], which is a Pair with zero values for both head and tail.
|
||||
// This function is useful for creating an empty Pair or as an identity element in monoid operations.
|
||||
//
|
||||
// The zero value for a Pair[L, R] has the zero value of type L as the head and the zero value
|
||||
// of type R as the tail. For reference types (pointers, slices, maps, channels, functions, interfaces),
|
||||
// the zero value is nil. For value types (numbers, booleans, structs), it's the type's zero value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Zero pair of integers
|
||||
// p1 := pair.Zero[int, int]() // Pair[int, int]{0, 0}
|
||||
//
|
||||
// // Zero pair of string and int
|
||||
// p2 := pair.Zero[string, int]() // Pair[string, int]{"", 0}
|
||||
//
|
||||
// // Zero pair with pointer types
|
||||
// p3 := pair.Zero[*int, *string]() // Pair[*int, *string]{nil, nil}
|
||||
func Zero[L, R any]() Pair[L, R] {
|
||||
return Pair[L, R]{}
|
||||
}
|
||||
|
||||
// Unpack extracts both values from a [Pair] and returns them as separate values.
|
||||
// This is the inverse operation of [MakePair], allowing you to destructure a Pair
|
||||
// back into its constituent head and tail values.
|
||||
//
|
||||
// This function is particularly useful when you need to work with both values
|
||||
// independently or pass them to functions that expect separate parameters rather
|
||||
// than a Pair.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// p := pair.MakePair("hello", 42)
|
||||
// head, tail := pair.Unpack(p) // head = "hello", tail = 42
|
||||
//
|
||||
// // Using with function that expects separate parameters
|
||||
// result := someFunc(pair.Unpack(p))
|
||||
//
|
||||
// // Destructuring for independent use
|
||||
// name, age := pair.Unpack(pair.MakePair("Alice", 30))
|
||||
// fmt.Printf("%s is %d years old\n", name, age)
|
||||
//
|
||||
//go:inline
|
||||
func Unpack[L, R any](p Pair[L, R]) (L, R) {
|
||||
return Head(p), Tail(p)
|
||||
}
|
||||
|
||||
@@ -16,575 +16,381 @@
|
||||
package pair
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
EQ "github.com/IBM/fp-go/v2/eq"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOf(t *testing.T) {
|
||||
p := Of(42)
|
||||
assert.Equal(t, 42, Head(p))
|
||||
assert.Equal(t, 42, Tail(p))
|
||||
// TestZeroWithIntegers tests Zero function with integer types
|
||||
func TestZeroWithIntegers(t *testing.T) {
|
||||
p := Zero[int, int]()
|
||||
|
||||
assert.Equal(t, 0, Head(p), "Head should be zero value for int")
|
||||
assert.Equal(t, 0, Tail(p), "Tail should be zero value for int")
|
||||
}
|
||||
|
||||
func TestMakePair(t *testing.T) {
|
||||
p := MakePair("hello", 42)
|
||||
assert.Equal(t, "hello", Head(p))
|
||||
assert.Equal(t, 42, Tail(p))
|
||||
// TestZeroWithStrings tests Zero function with string types
|
||||
func TestZeroWithStrings(t *testing.T) {
|
||||
p := Zero[string, string]()
|
||||
|
||||
assert.Equal(t, "", Head(p), "Head should be zero value for string")
|
||||
assert.Equal(t, "", Tail(p), "Tail should be zero value for string")
|
||||
}
|
||||
|
||||
func TestFromTuple(t *testing.T) {
|
||||
tup := tuple.MakeTuple2("world", 100)
|
||||
p := FromTuple(tup)
|
||||
assert.Equal(t, "world", Head(p))
|
||||
assert.Equal(t, 100, Tail(p))
|
||||
// TestZeroWithMixedTypes tests Zero function with different types
|
||||
func TestZeroWithMixedTypes(t *testing.T) {
|
||||
p := Zero[string, int]()
|
||||
|
||||
assert.Equal(t, "", Head(p), "Head should be zero value for string")
|
||||
assert.Equal(t, 0, Tail(p), "Tail should be zero value for int")
|
||||
}
|
||||
|
||||
func TestFromHead(t *testing.T) {
|
||||
// Test basic usage
|
||||
makePair := FromHead[int]("hello")
|
||||
p := makePair(42)
|
||||
assert.Equal(t, "hello", Head(p))
|
||||
assert.Equal(t, 42, Tail(p))
|
||||
// TestZeroWithBooleans tests Zero function with boolean types
|
||||
func TestZeroWithBooleans(t *testing.T) {
|
||||
p := Zero[bool, bool]()
|
||||
|
||||
// Test with different types
|
||||
makePair2 := FromHead[string](100)
|
||||
p2 := makePair2("world")
|
||||
assert.Equal(t, 100, Head(p2))
|
||||
assert.Equal(t, "world", Tail(p2))
|
||||
|
||||
// Test with same type for head and tail
|
||||
makePair3 := FromHead[int](1)
|
||||
p3 := makePair3(2)
|
||||
assert.Equal(t, 1, Head(p3))
|
||||
assert.Equal(t, 2, Tail(p3))
|
||||
assert.Equal(t, false, Head(p), "Head should be zero value for bool")
|
||||
assert.Equal(t, false, Tail(p), "Tail should be zero value for bool")
|
||||
}
|
||||
|
||||
func TestFromTail(t *testing.T) {
|
||||
// Test basic usage
|
||||
makePair := FromTail[string](42)
|
||||
p := makePair("hello")
|
||||
assert.Equal(t, "hello", Head(p))
|
||||
assert.Equal(t, 42, Tail(p))
|
||||
// TestZeroWithFloats tests Zero function with float types
|
||||
func TestZeroWithFloats(t *testing.T) {
|
||||
p := Zero[float64, float32]()
|
||||
|
||||
// Test with different types
|
||||
makePair2 := FromTail[int]("world")
|
||||
p2 := makePair2(100)
|
||||
assert.Equal(t, 100, Head(p2))
|
||||
assert.Equal(t, "world", Tail(p2))
|
||||
|
||||
// Test with same type for head and tail
|
||||
makePair3 := FromTail[int](2)
|
||||
p3 := makePair3(1)
|
||||
assert.Equal(t, 1, Head(p3))
|
||||
assert.Equal(t, 2, Tail(p3))
|
||||
assert.Equal(t, 0.0, Head(p), "Head should be zero value for float64")
|
||||
assert.Equal(t, float32(0.0), Tail(p), "Tail should be zero value for float32")
|
||||
}
|
||||
|
||||
func TestFromHeadFromTailComposition(t *testing.T) {
|
||||
// Test that FromHead and FromTail can be composed
|
||||
// and produce the same result as MakePair
|
||||
// TestZeroWithPointers tests Zero function with pointer types
|
||||
func TestZeroWithPointers(t *testing.T) {
|
||||
p := Zero[*int, *string]()
|
||||
|
||||
// Using FromHead
|
||||
fromHeadMaker := FromHead[int]("test")
|
||||
p1 := fromHeadMaker(123)
|
||||
|
||||
// Using FromTail
|
||||
fromTailMaker := FromTail[string](123)
|
||||
p2 := fromTailMaker("test")
|
||||
|
||||
// Using MakePair directly
|
||||
p3 := MakePair("test", 123)
|
||||
|
||||
// All three should produce the same result
|
||||
assert.Equal(t, Head(p1), Head(p2))
|
||||
assert.Equal(t, Tail(p1), Tail(p2))
|
||||
assert.Equal(t, Head(p1), Head(p3))
|
||||
assert.Equal(t, Tail(p1), Tail(p3))
|
||||
assert.Nil(t, Head(p), "Head should be nil for pointer type")
|
||||
assert.Nil(t, Tail(p), "Tail should be nil for pointer type")
|
||||
}
|
||||
|
||||
func TestToTuple(t *testing.T) {
|
||||
p := MakePair("hello", 42)
|
||||
tup := ToTuple(p)
|
||||
assert.Equal(t, "hello", tup.F1)
|
||||
assert.Equal(t, 42, tup.F2)
|
||||
// TestZeroWithSlices tests Zero function with slice types
|
||||
func TestZeroWithSlices(t *testing.T) {
|
||||
p := Zero[[]int, []string]()
|
||||
|
||||
assert.Nil(t, Head(p), "Head should be nil for slice type")
|
||||
assert.Nil(t, Tail(p), "Tail should be nil for slice type")
|
||||
}
|
||||
|
||||
func TestHeadAndTail(t *testing.T) {
|
||||
p := MakePair("test", 123)
|
||||
assert.Equal(t, "test", Head(p))
|
||||
assert.Equal(t, 123, Tail(p))
|
||||
// TestZeroWithMaps tests Zero function with map types
|
||||
func TestZeroWithMaps(t *testing.T) {
|
||||
p := Zero[map[string]int, map[int]string]()
|
||||
|
||||
assert.Nil(t, Head(p), "Head should be nil for map type")
|
||||
assert.Nil(t, Tail(p), "Tail should be nil for map type")
|
||||
}
|
||||
|
||||
func TestFirstAndSecond(t *testing.T) {
|
||||
p := MakePair("first", "second")
|
||||
assert.Equal(t, "first", First(p))
|
||||
assert.Equal(t, "second", Second(p))
|
||||
}
|
||||
|
||||
func TestMonadMapHead(t *testing.T) {
|
||||
p := MakePair(5, "hello")
|
||||
p2 := MonadMapHead(p, strconv.Itoa)
|
||||
assert.Equal(t, "5", Head(p2))
|
||||
assert.Equal(t, "hello", Tail(p2))
|
||||
}
|
||||
|
||||
func TestMonadMapTail(t *testing.T) {
|
||||
p := MakePair(5, "hello")
|
||||
p2 := MonadMapTail(p, func(s string) int {
|
||||
return len(s)
|
||||
})
|
||||
assert.Equal(t, 5, Head(p2))
|
||||
assert.Equal(t, 5, Tail(p2))
|
||||
}
|
||||
|
||||
func TestMonadBiMap(t *testing.T) {
|
||||
p := MakePair(5, "hello")
|
||||
p2 := MonadBiMap(p,
|
||||
strconv.Itoa,
|
||||
S.Size,
|
||||
)
|
||||
assert.Equal(t, "5", Head(p2))
|
||||
assert.Equal(t, 5, Tail(p2))
|
||||
}
|
||||
|
||||
func TestMapHead(t *testing.T) {
|
||||
mapper := MapHead[string](strconv.Itoa)
|
||||
p := MakePair(42, "world")
|
||||
p2 := mapper(p)
|
||||
assert.Equal(t, "42", Head(p2))
|
||||
assert.Equal(t, "world", Tail(p2))
|
||||
}
|
||||
|
||||
func TestMapTail(t *testing.T) {
|
||||
mapper := MapTail[int](func(s string) int {
|
||||
return len(s)
|
||||
})
|
||||
p := MakePair(10, "hello")
|
||||
p2 := mapper(p)
|
||||
assert.Equal(t, 10, Head(p2))
|
||||
assert.Equal(t, 5, Tail(p2))
|
||||
}
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
mapper := Map[int](func(s string) int {
|
||||
return len(s)
|
||||
})
|
||||
p := MakePair(10, "test")
|
||||
p2 := mapper(p)
|
||||
assert.Equal(t, 10, Head(p2))
|
||||
assert.Equal(t, 4, Tail(p2))
|
||||
}
|
||||
|
||||
func TestBiMap(t *testing.T) {
|
||||
mapper := BiMap(
|
||||
S.Format[int]("n=%d"),
|
||||
S.Size,
|
||||
)
|
||||
p := MakePair(7, "hello")
|
||||
p2 := mapper(p)
|
||||
assert.Equal(t, "n=7", Head(p2))
|
||||
assert.Equal(t, 5, Tail(p2))
|
||||
}
|
||||
|
||||
func TestSwap(t *testing.T) {
|
||||
p := MakePair("hello", 42)
|
||||
swapped := Swap(p)
|
||||
assert.Equal(t, 42, Head(swapped))
|
||||
assert.Equal(t, "hello", Tail(swapped))
|
||||
}
|
||||
|
||||
func TestMonadChainHead(t *testing.T) {
|
||||
strConcat := S.Semigroup
|
||||
p := MakePair(5, "hello")
|
||||
p2 := MonadChainHead(strConcat, p, func(n int) Pair[string, string] {
|
||||
return MakePair(fmt.Sprintf("%d", n), "!")
|
||||
})
|
||||
assert.Equal(t, "5", Head(p2))
|
||||
assert.Equal(t, "hello!", Tail(p2))
|
||||
}
|
||||
|
||||
func TestMonadChainTail(t *testing.T) {
|
||||
intSum := N.SemigroupSum[int]()
|
||||
p := MakePair(5, "hello")
|
||||
p2 := MonadChainTail(intSum, p, func(s string) Pair[int, int] {
|
||||
return MakePair(len(s), len(s)*2)
|
||||
})
|
||||
assert.Equal(t, 10, Head(p2)) // 5 + 5
|
||||
assert.Equal(t, 10, Tail(p2))
|
||||
}
|
||||
|
||||
func TestMonadChain(t *testing.T) {
|
||||
intSum := N.SemigroupSum[int]()
|
||||
p := MakePair(3, "test")
|
||||
p2 := MonadChain(intSum, p, func(s string) Pair[int, int] {
|
||||
return MakePair(len(s), len(s)*3)
|
||||
})
|
||||
assert.Equal(t, 7, Head(p2)) // 3 + 4
|
||||
assert.Equal(t, 12, Tail(p2))
|
||||
}
|
||||
|
||||
func TestChainHead(t *testing.T) {
|
||||
strConcat := S.Semigroup
|
||||
chain := ChainHead(strConcat, func(n int) Pair[string, string] {
|
||||
return MakePair(fmt.Sprintf("%d", n), "!")
|
||||
})
|
||||
p := MakePair(42, "hello")
|
||||
p2 := chain(p)
|
||||
assert.Equal(t, "42", Head(p2))
|
||||
assert.Equal(t, "hello!", Tail(p2))
|
||||
}
|
||||
|
||||
func TestChainTail(t *testing.T) {
|
||||
intSum := N.SemigroupSum[int]()
|
||||
chain := ChainTail(intSum, func(s string) Pair[int, int] {
|
||||
return MakePair(len(s), len(s)*2)
|
||||
})
|
||||
p := MakePair(10, "world")
|
||||
p2 := chain(p)
|
||||
assert.Equal(t, 15, Head(p2)) // 10 + 5
|
||||
assert.Equal(t, 10, Tail(p2))
|
||||
}
|
||||
|
||||
func TestChain(t *testing.T) {
|
||||
intSum := N.SemigroupSum[int]()
|
||||
chain := Chain(intSum, func(s string) Pair[int, int] {
|
||||
return MakePair(len(s), len(s)*2)
|
||||
})
|
||||
p := MakePair(5, "hi")
|
||||
p2 := chain(p)
|
||||
assert.Equal(t, 7, Head(p2)) // 5 + 2
|
||||
assert.Equal(t, 4, Tail(p2))
|
||||
}
|
||||
|
||||
func TestMonadApHead(t *testing.T) {
|
||||
strConcat := S.Semigroup
|
||||
pf := MakePair(strconv.Itoa, "!")
|
||||
pv := MakePair(42, "hello")
|
||||
result := MonadApHead(strConcat, pf, pv)
|
||||
assert.Equal(t, "42", Head(result))
|
||||
assert.Equal(t, "hello!", Tail(result))
|
||||
}
|
||||
|
||||
func TestMonadApTail(t *testing.T) {
|
||||
intSum := N.SemigroupSum[int]()
|
||||
pf := MakePair(10, S.Size)
|
||||
pv := MakePair(5, "hello")
|
||||
result := MonadApTail(intSum, pf, pv)
|
||||
assert.Equal(t, 15, Head(result)) // 5 + 10
|
||||
assert.Equal(t, 5, Tail(result))
|
||||
}
|
||||
|
||||
func TestMonadAp(t *testing.T) {
|
||||
intSum := N.SemigroupSum[int]()
|
||||
pf := MakePair(7, func(s string) int { return len(s) * 2 })
|
||||
pv := MakePair(3, "test")
|
||||
result := MonadAp(intSum, pf, pv)
|
||||
assert.Equal(t, 10, Head(result)) // 3 + 7
|
||||
assert.Equal(t, 8, Tail(result)) // len("test") * 2
|
||||
}
|
||||
|
||||
func TestApHead(t *testing.T) {
|
||||
strConcat := S.Semigroup
|
||||
pv := MakePair(100, "world")
|
||||
ap := ApHead[string, int, string](strConcat, pv)
|
||||
pf := MakePair(func(n int) string { return fmt.Sprintf("num=%d", n) }, "!")
|
||||
result := ap(pf)
|
||||
assert.Equal(t, "num=100", Head(result))
|
||||
assert.Equal(t, "world!", Tail(result))
|
||||
}
|
||||
|
||||
func TestApTail(t *testing.T) {
|
||||
intSum := N.SemigroupSum[int]()
|
||||
pv := MakePair(20, "hello")
|
||||
ap := ApTail[int, string, int](intSum, pv)
|
||||
pf := MakePair(5, S.Size)
|
||||
result := ap(pf)
|
||||
assert.Equal(t, 25, Head(result)) // 20 + 5
|
||||
assert.Equal(t, 5, Tail(result))
|
||||
}
|
||||
|
||||
func TestAp(t *testing.T) {
|
||||
intSum := N.SemigroupSum[int]()
|
||||
pv := MakePair(15, "test")
|
||||
ap := Ap[int, string, int](intSum, pv)
|
||||
pf := MakePair(10, func(s string) int { return len(s) * 3 })
|
||||
result := ap(pf)
|
||||
assert.Equal(t, 25, Head(result)) // 15 + 10
|
||||
assert.Equal(t, 12, Tail(result)) // len("test") * 3
|
||||
}
|
||||
|
||||
func TestPaired(t *testing.T) {
|
||||
add := func(a, b int) int { return a + b }
|
||||
pairedAdd := Paired(add)
|
||||
result := pairedAdd(MakePair(3, 4))
|
||||
assert.Equal(t, 7, result)
|
||||
}
|
||||
|
||||
func TestUnpaired(t *testing.T) {
|
||||
pairedAdd := func(p Pair[int, int]) int {
|
||||
return Head(p) + Tail(p)
|
||||
// TestZeroWithStructs tests Zero function with struct types
|
||||
func TestZeroWithStructs(t *testing.T) {
|
||||
type TestStruct struct {
|
||||
Field1 int
|
||||
Field2 string
|
||||
}
|
||||
add := Unpaired(pairedAdd)
|
||||
result := add(5, 7)
|
||||
assert.Equal(t, 12, result)
|
||||
|
||||
p := Zero[TestStruct, TestStruct]()
|
||||
|
||||
expected := TestStruct{Field1: 0, Field2: ""}
|
||||
assert.Equal(t, expected, Head(p), "Head should be zero value for struct")
|
||||
assert.Equal(t, expected, Tail(p), "Tail should be zero value for struct")
|
||||
}
|
||||
|
||||
func TestMerge(t *testing.T) {
|
||||
add := N.Add[int]
|
||||
merge := Merge(add)
|
||||
result := merge(MakePair(3, 4))
|
||||
assert.Equal(t, 7, result)
|
||||
// TestZeroWithInterfaces tests Zero function with interface types
|
||||
func TestZeroWithInterfaces(t *testing.T) {
|
||||
p := Zero[interface{}, interface{}]()
|
||||
|
||||
assert.Nil(t, Head(p), "Head should be nil for interface type")
|
||||
assert.Nil(t, Tail(p), "Tail should be nil for interface type")
|
||||
}
|
||||
|
||||
func TestEq(t *testing.T) {
|
||||
pairEq := Eq(
|
||||
EQ.FromStrictEquals[string](),
|
||||
EQ.FromStrictEquals[int](),
|
||||
)
|
||||
p1 := MakePair("hello", 42)
|
||||
p2 := MakePair("hello", 42)
|
||||
p3 := MakePair("world", 42)
|
||||
p4 := MakePair("hello", 100)
|
||||
// TestZeroWithChannels tests Zero function with channel types
|
||||
func TestZeroWithChannels(t *testing.T) {
|
||||
p := Zero[chan int, chan string]()
|
||||
|
||||
assert.True(t, pairEq.Equals(p1, p2))
|
||||
assert.False(t, pairEq.Equals(p1, p3))
|
||||
assert.False(t, pairEq.Equals(p1, p4))
|
||||
assert.Nil(t, Head(p), "Head should be nil for channel type")
|
||||
assert.Nil(t, Tail(p), "Tail should be nil for channel type")
|
||||
}
|
||||
|
||||
func TestFromStrictEquals(t *testing.T) {
|
||||
pairEq := FromStrictEquals[string, int]()
|
||||
p1 := MakePair("test", 123)
|
||||
p2 := MakePair("test", 123)
|
||||
p3 := MakePair("test", 456)
|
||||
// TestZeroWithFunctions tests Zero function with function types
|
||||
func TestZeroWithFunctions(t *testing.T) {
|
||||
p := Zero[func() int, func(string) bool]()
|
||||
|
||||
assert.True(t, pairEq.Equals(p1, p2))
|
||||
assert.False(t, pairEq.Equals(p1, p3))
|
||||
assert.Nil(t, Head(p), "Head should be nil for function type")
|
||||
assert.Nil(t, Tail(p), "Tail should be nil for function type")
|
||||
}
|
||||
|
||||
func TestMonadHead(t *testing.T) {
|
||||
stringMonoid := S.Monoid
|
||||
monad := MonadHead[int, string, string](stringMonoid)
|
||||
// TestZeroCanBeUsedWithOtherFunctions tests that Zero pairs work with other pair functions
|
||||
func TestZeroCanBeUsedWithOtherFunctions(t *testing.T) {
|
||||
p := Zero[int, string]()
|
||||
|
||||
// Test Of
|
||||
p := monad.Of(42)
|
||||
assert.Equal(t, 42, Head(p))
|
||||
// Test with Head and Tail
|
||||
assert.Equal(t, 0, Head(p))
|
||||
assert.Equal(t, "", Tail(p))
|
||||
|
||||
// Test Map
|
||||
mapper := monad.Map(strconv.Itoa)
|
||||
p2 := mapper(MakePair(100, "!"))
|
||||
assert.Equal(t, "100", Head(p2))
|
||||
assert.Equal(t, "!", Tail(p2))
|
||||
// Test with First and Second
|
||||
assert.Equal(t, 0, First(p))
|
||||
assert.Equal(t, "", Second(p))
|
||||
|
||||
// Test Chain
|
||||
chain := monad.Chain(func(n int) Pair[string, string] {
|
||||
return MakePair(fmt.Sprintf("n=%d", n), "!")
|
||||
})
|
||||
p3 := chain(MakePair(7, "hello"))
|
||||
assert.Equal(t, "n=7", Head(p3))
|
||||
assert.Equal(t, "hello!", Tail(p3))
|
||||
|
||||
// Test Ap
|
||||
pv := MakePair(5, "world")
|
||||
ap := monad.Ap(pv)
|
||||
pf := MakePair(func(n int) string { return fmt.Sprintf("%d", n*2) }, "!")
|
||||
p4 := ap(pf)
|
||||
assert.Equal(t, "10", Head(p4))
|
||||
assert.Equal(t, "world!", Tail(p4))
|
||||
}
|
||||
|
||||
func TestPointedHead(t *testing.T) {
|
||||
stringMonoid := S.Monoid
|
||||
pointed := PointedHead[int](stringMonoid)
|
||||
p := pointed.Of(42)
|
||||
assert.Equal(t, 42, Head(p))
|
||||
assert.Equal(t, "", Tail(p))
|
||||
}
|
||||
|
||||
func TestFunctorHead(t *testing.T) {
|
||||
functor := FunctorHead[int, string, string]()
|
||||
mapper := functor.Map(func(n int) string { return fmt.Sprintf("value=%d", n) })
|
||||
p := MakePair(42, "test")
|
||||
p2 := mapper(p)
|
||||
assert.Equal(t, "value=42", Head(p2))
|
||||
assert.Equal(t, "test", Tail(p2))
|
||||
}
|
||||
|
||||
func TestApplicativeHead(t *testing.T) {
|
||||
stringMonoid := S.Monoid
|
||||
applicative := ApplicativeHead[int, string, string](stringMonoid)
|
||||
|
||||
// Test Of
|
||||
p := applicative.Of(100)
|
||||
assert.Equal(t, 100, Head(p))
|
||||
assert.Equal(t, "", Tail(p))
|
||||
|
||||
// Test Map
|
||||
mapper := applicative.Map(strconv.Itoa)
|
||||
p2 := mapper(MakePair(42, "!"))
|
||||
assert.Equal(t, "42", Head(p2))
|
||||
assert.Equal(t, "!", Tail(p2))
|
||||
|
||||
// Test Ap
|
||||
pv := MakePair(7, "hello")
|
||||
ap := applicative.Ap(pv)
|
||||
pf := MakePair(func(n int) string { return fmt.Sprintf("n=%d", n) }, "!")
|
||||
p3 := ap(pf)
|
||||
assert.Equal(t, "n=7", Head(p3))
|
||||
assert.Equal(t, "hello!", Tail(p3))
|
||||
}
|
||||
|
||||
func TestMonadTail(t *testing.T) {
|
||||
intSum := N.MonoidSum[int]()
|
||||
monad := MonadTail[string, int, int](intSum)
|
||||
|
||||
// Test Of
|
||||
p := monad.Of("hello")
|
||||
assert.Equal(t, 0, Head(p))
|
||||
assert.Equal(t, "hello", Tail(p))
|
||||
|
||||
// Test Map
|
||||
mapper := monad.Map(S.Size)
|
||||
p2 := mapper(MakePair(5, "world"))
|
||||
assert.Equal(t, 5, Head(p2))
|
||||
assert.Equal(t, 5, Tail(p2))
|
||||
|
||||
// Test Chain
|
||||
chain := monad.Chain(func(s string) Pair[int, int] {
|
||||
return MakePair(len(s), len(s)*2)
|
||||
})
|
||||
p3 := chain(MakePair(10, "test"))
|
||||
assert.Equal(t, 14, Head(p3)) // 10 + 4
|
||||
assert.Equal(t, 8, Tail(p3))
|
||||
|
||||
// Test Ap
|
||||
pv := MakePair(5, "hello")
|
||||
ap := monad.Ap(pv)
|
||||
pf := MakePair(10, S.Size)
|
||||
p4 := ap(pf)
|
||||
assert.Equal(t, 15, Head(p4)) // 5 + 10
|
||||
assert.Equal(t, 5, Tail(p4))
|
||||
}
|
||||
|
||||
func TestPointedTail(t *testing.T) {
|
||||
intSum := N.MonoidSum[int]()
|
||||
pointed := PointedTail[string](intSum)
|
||||
p := pointed.Of("test")
|
||||
assert.Equal(t, 0, Head(p))
|
||||
assert.Equal(t, "test", Tail(p))
|
||||
}
|
||||
|
||||
func TestFunctorTail(t *testing.T) {
|
||||
functor := FunctorTail[string, int, int]()
|
||||
mapper := functor.Map(func(s string) int { return len(s) * 2 })
|
||||
p := MakePair(10, "hello")
|
||||
p2 := mapper(p)
|
||||
assert.Equal(t, 10, Head(p2))
|
||||
assert.Equal(t, 10, Tail(p2))
|
||||
}
|
||||
|
||||
func TestApplicativeTail(t *testing.T) {
|
||||
intSum := N.MonoidSum[int]()
|
||||
applicative := ApplicativeTail[string, int, int](intSum)
|
||||
|
||||
// Test Of
|
||||
p := applicative.Of("world")
|
||||
assert.Equal(t, 0, Head(p))
|
||||
assert.Equal(t, "world", Tail(p))
|
||||
|
||||
// Test Map
|
||||
mapper := applicative.Map(S.Size)
|
||||
p2 := mapper(MakePair(5, "test"))
|
||||
assert.Equal(t, 5, Head(p2))
|
||||
assert.Equal(t, 4, Tail(p2))
|
||||
|
||||
// Test Ap
|
||||
pv := MakePair(10, "hello")
|
||||
ap := applicative.Ap(pv)
|
||||
pf := MakePair(5, func(s string) int { return len(s) * 2 })
|
||||
p3 := ap(pf)
|
||||
assert.Equal(t, 15, Head(p3)) // 10 + 5
|
||||
assert.Equal(t, 10, Tail(p3))
|
||||
}
|
||||
|
||||
func TestMonad(t *testing.T) {
|
||||
intSum := N.MonoidSum[int]()
|
||||
monad := Monad[string, int, int](intSum)
|
||||
|
||||
p := monad.Of("test")
|
||||
assert.Equal(t, 0, Head(p))
|
||||
assert.Equal(t, "test", Tail(p))
|
||||
}
|
||||
|
||||
func TestPointed(t *testing.T) {
|
||||
intSum := N.MonoidSum[int]()
|
||||
pointed := Pointed[string](intSum)
|
||||
|
||||
p := pointed.Of("hello")
|
||||
assert.Equal(t, 0, Head(p))
|
||||
assert.Equal(t, "hello", Tail(p))
|
||||
}
|
||||
|
||||
func TestFunctor(t *testing.T) {
|
||||
functor := Functor[string, int, int]()
|
||||
mapper := functor.Map(S.Size)
|
||||
p := MakePair(7, "world")
|
||||
p2 := mapper(p)
|
||||
assert.Equal(t, 7, Head(p2))
|
||||
assert.Equal(t, 5, Tail(p2))
|
||||
}
|
||||
|
||||
func TestApplicative(t *testing.T) {
|
||||
intSum := N.MonoidSum[int]()
|
||||
applicative := Applicative[string, int, int](intSum)
|
||||
|
||||
p := applicative.Of("test")
|
||||
assert.Equal(t, 0, Head(p))
|
||||
assert.Equal(t, "test", Tail(p))
|
||||
}
|
||||
|
||||
// Test edge cases and complex scenarios
|
||||
func TestComplexChaining(t *testing.T) {
|
||||
intSum := N.SemigroupSum[int]()
|
||||
|
||||
// Chain multiple operations
|
||||
p := MakePair(1, "a")
|
||||
p2 := MonadChainTail(intSum, p, func(s string) Pair[int, string] {
|
||||
return MakePair(len(s), s+"b")
|
||||
})
|
||||
p3 := MonadChainTail(intSum, p2, func(s string) Pair[int, string] {
|
||||
return MakePair(len(s), s+"c")
|
||||
})
|
||||
|
||||
assert.Equal(t, 4, Head(p3)) // 1 + 1 + 2
|
||||
assert.Equal(t, "abc", Tail(p3))
|
||||
}
|
||||
|
||||
func TestBiMapWithDifferentTypes(t *testing.T) {
|
||||
p := MakePair(3.14, true)
|
||||
p2 := MonadBiMap(p,
|
||||
func(f float64) int { return int(f * 10) },
|
||||
func(b bool) string {
|
||||
if b {
|
||||
return "yes"
|
||||
}
|
||||
return "no"
|
||||
},
|
||||
)
|
||||
assert.Equal(t, 31, Head(p2))
|
||||
assert.Equal(t, "yes", Tail(p2))
|
||||
}
|
||||
|
||||
func TestSwapTwice(t *testing.T) {
|
||||
p := MakePair("original", 999)
|
||||
// Test with Swap
|
||||
swapped := Swap(p)
|
||||
swappedBack := Swap(swapped)
|
||||
assert.Equal(t, "original", Head(swappedBack))
|
||||
assert.Equal(t, 999, Tail(swappedBack))
|
||||
assert.Equal(t, "", Head(swapped))
|
||||
assert.Equal(t, 0, Tail(swapped))
|
||||
|
||||
// Test with Map
|
||||
mapped := MonadMapTail(p, func(s string) int { return len(s) })
|
||||
assert.Equal(t, 0, Head(mapped))
|
||||
assert.Equal(t, 0, Tail(mapped))
|
||||
}
|
||||
|
||||
// TestZeroEquality tests that multiple Zero calls produce equal pairs
|
||||
func TestZeroEquality(t *testing.T) {
|
||||
p1 := Zero[int, string]()
|
||||
p2 := Zero[int, string]()
|
||||
|
||||
assert.Equal(t, Head(p1), Head(p2), "Heads should be equal")
|
||||
assert.Equal(t, Tail(p1), Tail(p2), "Tails should be equal")
|
||||
}
|
||||
|
||||
// TestZeroWithComplexTypes tests Zero with more complex nested types
|
||||
func TestZeroWithComplexTypes(t *testing.T) {
|
||||
type ComplexType struct {
|
||||
Nested map[string][]int
|
||||
Ptr *string
|
||||
}
|
||||
|
||||
p := Zero[ComplexType, []map[string]int]()
|
||||
|
||||
expectedHead := ComplexType{Nested: nil, Ptr: nil}
|
||||
assert.Equal(t, expectedHead, Head(p), "Head should be zero value for complex struct")
|
||||
assert.Nil(t, Tail(p), "Tail should be nil for slice of maps")
|
||||
}
|
||||
|
||||
// TestUnpackWithIntegers tests Unpack function with integer types
|
||||
func TestUnpackWithIntegers(t *testing.T) {
|
||||
p := MakePair(42, 100)
|
||||
head, tail := Unpack(p)
|
||||
|
||||
assert.Equal(t, 42, head, "Head should be 42")
|
||||
assert.Equal(t, 100, tail, "Tail should be 100")
|
||||
}
|
||||
|
||||
// TestUnpackWithStrings tests Unpack function with string types
|
||||
func TestUnpackWithStrings(t *testing.T) {
|
||||
p := MakePair("hello", "world")
|
||||
head, tail := Unpack(p)
|
||||
|
||||
assert.Equal(t, "hello", head, "Head should be 'hello'")
|
||||
assert.Equal(t, "world", tail, "Tail should be 'world'")
|
||||
}
|
||||
|
||||
// TestUnpackWithMixedTypes tests Unpack function with different types
|
||||
func TestUnpackWithMixedTypes(t *testing.T) {
|
||||
p := MakePair("Alice", 30)
|
||||
name, age := Unpack(p)
|
||||
|
||||
assert.Equal(t, "Alice", name, "Name should be 'Alice'")
|
||||
assert.Equal(t, 30, age, "Age should be 30")
|
||||
}
|
||||
|
||||
// TestUnpackWithBooleans tests Unpack function with boolean types
|
||||
func TestUnpackWithBooleans(t *testing.T) {
|
||||
p := MakePair(true, false)
|
||||
head, tail := Unpack(p)
|
||||
|
||||
assert.Equal(t, true, head, "Head should be true")
|
||||
assert.Equal(t, false, tail, "Tail should be false")
|
||||
}
|
||||
|
||||
// TestUnpackWithFloats tests Unpack function with float types
|
||||
func TestUnpackWithFloats(t *testing.T) {
|
||||
p := MakePair(3.14, float32(2.71))
|
||||
head, tail := Unpack(p)
|
||||
|
||||
assert.Equal(t, 3.14, head, "Head should be 3.14")
|
||||
assert.Equal(t, float32(2.71), tail, "Tail should be 2.71")
|
||||
}
|
||||
|
||||
// TestUnpackWithPointers tests Unpack function with pointer types
|
||||
func TestUnpackWithPointers(t *testing.T) {
|
||||
x := 42
|
||||
y := "test"
|
||||
p := MakePair(&x, &y)
|
||||
head, tail := Unpack(p)
|
||||
|
||||
assert.Equal(t, &x, head, "Head should point to x")
|
||||
assert.Equal(t, &y, tail, "Tail should point to y")
|
||||
assert.Equal(t, 42, *head, "Dereferenced head should be 42")
|
||||
assert.Equal(t, "test", *tail, "Dereferenced tail should be 'test'")
|
||||
}
|
||||
|
||||
// TestUnpackWithSlices tests Unpack function with slice types
|
||||
func TestUnpackWithSlices(t *testing.T) {
|
||||
p := MakePair([]int{1, 2, 3}, []string{"a", "b", "c"})
|
||||
head, tail := Unpack(p)
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3}, head, "Head should be [1, 2, 3]")
|
||||
assert.Equal(t, []string{"a", "b", "c"}, tail, "Tail should be ['a', 'b', 'c']")
|
||||
}
|
||||
|
||||
// TestUnpackWithMaps tests Unpack function with map types
|
||||
func TestUnpackWithMaps(t *testing.T) {
|
||||
m1 := map[string]int{"one": 1, "two": 2}
|
||||
m2 := map[int]string{1: "one", 2: "two"}
|
||||
p := MakePair(m1, m2)
|
||||
head, tail := Unpack(p)
|
||||
|
||||
assert.Equal(t, m1, head, "Head should be the first map")
|
||||
assert.Equal(t, m2, tail, "Tail should be the second map")
|
||||
}
|
||||
|
||||
// TestUnpackWithStructs tests Unpack function with struct types
|
||||
func TestUnpackWithStructs(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
p1 := Person{Name: "Alice", Age: 30}
|
||||
p2 := Person{Name: "Bob", Age: 25}
|
||||
p := MakePair(p1, p2)
|
||||
head, tail := Unpack(p)
|
||||
|
||||
assert.Equal(t, p1, head, "Head should be Alice")
|
||||
assert.Equal(t, p2, tail, "Tail should be Bob")
|
||||
}
|
||||
|
||||
// TestUnpackWithFunctions tests Unpack function with function types
|
||||
func TestUnpackWithFunctions(t *testing.T) {
|
||||
f1 := func(x int) int { return x * 2 }
|
||||
f2 := func(x int) int { return x + 10 }
|
||||
p := MakePair(f1, f2)
|
||||
head, tail := Unpack(p)
|
||||
|
||||
assert.Equal(t, 20, head(10), "Head function should double the input")
|
||||
assert.Equal(t, 20, tail(10), "Tail function should add 10 to the input")
|
||||
}
|
||||
|
||||
// TestUnpackWithZeroPair tests Unpack function with zero-valued pair
|
||||
func TestUnpackWithZeroPair(t *testing.T) {
|
||||
p := Zero[int, string]()
|
||||
head, tail := Unpack(p)
|
||||
|
||||
assert.Equal(t, 0, head, "Head should be zero value for int")
|
||||
assert.Equal(t, "", tail, "Tail should be zero value for string")
|
||||
}
|
||||
|
||||
// TestUnpackWithNilValues tests Unpack function with nil values
|
||||
func TestUnpackWithNilValues(t *testing.T) {
|
||||
p := MakePair[*int, *string](nil, nil)
|
||||
head, tail := Unpack(p)
|
||||
|
||||
assert.Nil(t, head, "Head should be nil")
|
||||
assert.Nil(t, tail, "Tail should be nil")
|
||||
}
|
||||
|
||||
// TestUnpackInverseMakePair tests that Unpack is the inverse of MakePair
|
||||
func TestUnpackInverseMakePair(t *testing.T) {
|
||||
original := MakePair("test", 123)
|
||||
head, tail := Unpack(original)
|
||||
reconstructed := MakePair(head, tail)
|
||||
|
||||
assert.Equal(t, Head(original), Head(reconstructed), "Heads should be equal")
|
||||
assert.Equal(t, Tail(original), Tail(reconstructed), "Tails should be equal")
|
||||
}
|
||||
|
||||
// TestUnpackWithOf tests Unpack with a pair created by Of
|
||||
func TestUnpackWithOf(t *testing.T) {
|
||||
p := Of(42)
|
||||
head, tail := Unpack(p)
|
||||
|
||||
assert.Equal(t, 42, head, "Head should be 42")
|
||||
assert.Equal(t, 42, tail, "Tail should be 42")
|
||||
}
|
||||
|
||||
// TestUnpackWithSwap tests Unpack after swapping a pair
|
||||
func TestUnpackWithSwap(t *testing.T) {
|
||||
original := MakePair("hello", 42)
|
||||
swapped := Swap(original)
|
||||
head, tail := Unpack(swapped)
|
||||
|
||||
assert.Equal(t, 42, head, "Head should be 42 after swap")
|
||||
assert.Equal(t, "hello", tail, "Tail should be 'hello' after swap")
|
||||
}
|
||||
|
||||
// TestUnpackWithMappedPair tests Unpack with a mapped pair
|
||||
func TestUnpackWithMappedPair(t *testing.T) {
|
||||
original := MakePair(5, "hello")
|
||||
mapped := MonadMapTail(original, func(s string) int { return len(s) })
|
||||
head, tail := Unpack(mapped)
|
||||
|
||||
assert.Equal(t, 5, head, "Head should remain 5")
|
||||
assert.Equal(t, 5, tail, "Tail should be length of 'hello'")
|
||||
}
|
||||
|
||||
// TestUnpackWithComplexTypes tests Unpack with complex nested types
|
||||
func TestUnpackWithComplexTypes(t *testing.T) {
|
||||
type ComplexType struct {
|
||||
Data map[string][]int
|
||||
Nested *ComplexType
|
||||
}
|
||||
|
||||
c1 := ComplexType{
|
||||
Data: map[string][]int{"key": {1, 2, 3}},
|
||||
Nested: nil,
|
||||
}
|
||||
c2 := ComplexType{
|
||||
Data: map[string][]int{"other": {4, 5, 6}},
|
||||
Nested: &c1,
|
||||
}
|
||||
|
||||
p := MakePair(c1, c2)
|
||||
head, tail := Unpack(p)
|
||||
|
||||
assert.Equal(t, c1, head, "Head should be c1")
|
||||
assert.Equal(t, c2, tail, "Tail should be c2")
|
||||
assert.NotNil(t, tail.Nested, "Tail's nested field should not be nil")
|
||||
}
|
||||
|
||||
// TestUnpackMultipleAssignments tests that Unpack can be used in multiple assignments
|
||||
func TestUnpackMultipleAssignments(t *testing.T) {
|
||||
p1 := MakePair(1, "one")
|
||||
p2 := MakePair(2, "two")
|
||||
|
||||
h1, t1 := Unpack(p1)
|
||||
h2, t2 := Unpack(p2)
|
||||
|
||||
assert.Equal(t, 1, h1)
|
||||
assert.Equal(t, "one", t1)
|
||||
assert.Equal(t, 2, h2)
|
||||
assert.Equal(t, "two", t2)
|
||||
}
|
||||
|
||||
// TestUnpackWithChannels tests Unpack function with channel types
|
||||
func TestUnpackWithChannels(t *testing.T) {
|
||||
ch1 := make(chan int, 1)
|
||||
ch2 := make(chan string, 1)
|
||||
ch1 <- 42
|
||||
ch2 <- "test"
|
||||
|
||||
p := MakePair(ch1, ch2)
|
||||
head, tail := Unpack(p)
|
||||
|
||||
assert.Equal(t, 42, <-head, "Should receive 42 from head channel")
|
||||
assert.Equal(t, "test", <-tail, "Should receive 'test' from tail channel")
|
||||
}
|
||||
|
||||
// TestUnpackWithInterfaces tests Unpack function with interface types
|
||||
func TestUnpackWithInterfaces(t *testing.T) {
|
||||
var i1 interface{} = 42
|
||||
var i2 interface{} = "test"
|
||||
|
||||
p := MakePair(i1, i2)
|
||||
head, tail := Unpack(p)
|
||||
|
||||
assert.Equal(t, 42, head, "Head should be 42")
|
||||
assert.Equal(t, "test", tail, "Tail should be 'test'")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
// 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 (
|
||||
R "reflect"
|
||||
)
|
||||
|
||||
func Map[GA ~[]A, A any](f func(R.Value) A) func(R.Value) GA {
|
||||
return func(val R.Value) GA {
|
||||
l := val.Len()
|
||||
res := make(GA, l)
|
||||
for i := l - 1; i >= 0; i-- {
|
||||
res[i] = f(val.Index(i))
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
||||
@@ -24,10 +24,28 @@ package reflect
|
||||
import (
|
||||
R "reflect"
|
||||
|
||||
"github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
G "github.com/IBM/fp-go/v2/reflect/generic"
|
||||
)
|
||||
|
||||
func MonadReduceWithIndex[A any](val R.Value, f func(int, A, R.Value) A, initial A) A {
|
||||
|
||||
kind := val.Kind()
|
||||
|
||||
// Check if it supports Len() and Index()
|
||||
if kind != R.Slice && kind != R.Array && kind != R.String {
|
||||
// Not a sequential iterable, return initial
|
||||
return initial
|
||||
}
|
||||
|
||||
count := val.Len()
|
||||
current := initial
|
||||
for i := range count {
|
||||
current = f(i, current, val.Index(i))
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
// ReduceWithIndex applies a reducer function to each element of a reflect.Value (representing a slice or array),
|
||||
// accumulating a result value. The reducer function receives the current index, the accumulated value,
|
||||
// and the current element as a reflect.Value.
|
||||
@@ -52,12 +70,7 @@ import (
|
||||
// // result = 0 + (0+10) + (1+20) + (2+30) = 63
|
||||
func ReduceWithIndex[A any](f func(int, A, R.Value) A, initial A) func(R.Value) A {
|
||||
return func(val R.Value) A {
|
||||
count := val.Len()
|
||||
current := initial
|
||||
for i := range count {
|
||||
current = f(i, current, val.Index(i))
|
||||
}
|
||||
return current
|
||||
return MonadReduceWithIndex(val, f, initial)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +99,71 @@ func Reduce[A any](f func(A, R.Value) A, initial A) func(R.Value) A {
|
||||
return ReduceWithIndex(F.Ignore1of3[int](f), initial)
|
||||
}
|
||||
|
||||
// MonadMapWithIndex is the non-curried version of MapWithIndex. It transforms each element of a
|
||||
// reflect.Value (representing a slice, array, or string) using the provided function that receives
|
||||
// both the index and the element, returning a new slice containing the transformed values.
|
||||
//
|
||||
// Unlike MapWithIndex which is curried, this function takes both the reflect.Value and the
|
||||
// transformation function as parameters in a single call. This is useful when you need to pass
|
||||
// the function directly without partial application.
|
||||
//
|
||||
// Parameters:
|
||||
// - val: The reflect.Value to map over (must be a slice, array, or string)
|
||||
// - f: A transformation function that takes (index int, element reflect.Value) and returns a value of type A
|
||||
//
|
||||
// Returns:
|
||||
// - A slice of transformed values, or an empty slice if val is not iterable
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Transform a reflected slice with index awareness
|
||||
// input := reflect.ValueOf([]int{10, 20, 30})
|
||||
// result := MonadMapWithIndex(input, func(i int, v reflect.Value) string {
|
||||
// return fmt.Sprintf("[%d]=%d", i, int(v.Int()))
|
||||
// })
|
||||
// // result = []string{"[0]=10", "[1]=20", "[2]=30"}
|
||||
func MonadMapWithIndex[A any](val R.Value, f func(int, R.Value) A) []A {
|
||||
|
||||
kind := val.Kind()
|
||||
|
||||
// Check if it supports Len() and Index()
|
||||
if kind != R.Slice && kind != R.Array && kind != R.String {
|
||||
// Not a sequential iterable, return initial
|
||||
return array.Empty[A]()
|
||||
}
|
||||
|
||||
l := val.Len()
|
||||
res := make([]A, l)
|
||||
for i := l - 1; i >= 0; i-- {
|
||||
res[i] = f(i, val.Index(i))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// MapWithIndex transforms each element of a reflect.Value (representing a slice or array) using the provided
|
||||
// function that receives both the index and the element, returning a new slice containing the transformed values.
|
||||
//
|
||||
// This is a curried function that first takes the transformation function,
|
||||
// then returns a function that accepts the reflect.Value to map over.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A transformation function that takes (index int, element reflect.Value) and returns a value of type A
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a reflect.Value and returns a slice of transformed values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create indexed labels from a reflected slice
|
||||
// indexedLabels := MapWithIndex(func(i int, v reflect.Value) string {
|
||||
// return fmt.Sprintf("[%d]: %d", i, int(v.Int()))
|
||||
// })
|
||||
// result := indexedLabels(reflect.ValueOf([]int{10, 20, 30}))
|
||||
// // result = []string{"[0]: 10", "[1]: 20", "[2]: 30"}
|
||||
func MapWithIndex[A any](f func(int, R.Value) A) func(R.Value) []A {
|
||||
return F.Bind2nd(MonadMapWithIndex, f)
|
||||
}
|
||||
|
||||
// Map transforms each element of a reflect.Value (representing a slice or array) using the provided
|
||||
// function, returning a new slice containing the transformed values.
|
||||
//
|
||||
@@ -107,5 +185,5 @@ func Reduce[A any](f func(A, R.Value) A, initial A) func(R.Value) A {
|
||||
// result := doubleInts(reflect.ValueOf([]int{1, 2, 3}))
|
||||
// // result = []int{2, 4, 6}
|
||||
func Map[A any](f func(R.Value) A) func(R.Value) []A {
|
||||
return G.Map[[]A](f)
|
||||
return MapWithIndex(F.Ignore1of2[int](f))
|
||||
}
|
||||
|
||||
@@ -369,3 +369,446 @@ func TestIntegration_ReduceWithIndexToMap(t *testing.T) {
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
// TestMapWithIndex_IntToString tests mapping integers to strings with index
|
||||
func TestMapWithIndex_IntToString(t *testing.T) {
|
||||
input := []int{10, 20, 30}
|
||||
reflectVal := reflect.ValueOf(input)
|
||||
|
||||
mapper := MapWithIndex(func(i int, v reflect.Value) string {
|
||||
return string(rune('A'+i)) + ":" + string(rune('0'+int(v.Int()/10)))
|
||||
})
|
||||
|
||||
result := mapper(reflectVal)
|
||||
expected := []string{"A:1", "B:2", "C:3"}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
// TestMapWithIndex_WithIndexCalculation tests using index in calculation
|
||||
func TestMapWithIndex_WithIndexCalculation(t *testing.T) {
|
||||
input := []int{5, 10, 15}
|
||||
reflectVal := reflect.ValueOf(input)
|
||||
|
||||
// Multiply value by its index
|
||||
mapper := MapWithIndex(func(i int, v reflect.Value) int {
|
||||
return i * int(v.Int())
|
||||
})
|
||||
|
||||
result := mapper(reflectVal)
|
||||
expected := []int{0, 10, 30} // 0*5, 1*10, 2*15
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
// TestMapWithIndex_EmptySlice tests mapping an empty slice with index
|
||||
func TestMapWithIndex_EmptySlice(t *testing.T) {
|
||||
input := []int{}
|
||||
reflectVal := reflect.ValueOf(input)
|
||||
|
||||
mapper := MapWithIndex(func(i int, v reflect.Value) int {
|
||||
return i + int(v.Int())
|
||||
})
|
||||
|
||||
result := mapper(reflectVal)
|
||||
assert.Empty(t, result, "Should return empty slice")
|
||||
assert.NotNil(t, result, "Should not return nil")
|
||||
}
|
||||
|
||||
// TestMapWithIndex_SingleElement tests mapping a single-element slice with index
|
||||
func TestMapWithIndex_SingleElement(t *testing.T) {
|
||||
input := []int{42}
|
||||
reflectVal := reflect.ValueOf(input)
|
||||
|
||||
mapper := MapWithIndex(func(i int, v reflect.Value) string {
|
||||
return string(rune('0'+i)) + ":" + string(rune('0'+int(v.Int()/10)))
|
||||
})
|
||||
|
||||
result := mapper(reflectVal)
|
||||
expected := []string{"0:4"}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
// TestMapWithIndex_ComplexStruct tests mapping with index to build complex structures
|
||||
func TestMapWithIndex_ComplexStruct(t *testing.T) {
|
||||
input := []string{"apple", "banana", "cherry"}
|
||||
reflectVal := reflect.ValueOf(input)
|
||||
|
||||
type IndexedItem struct {
|
||||
Index int
|
||||
Value string
|
||||
Len int
|
||||
}
|
||||
|
||||
mapper := MapWithIndex(func(i int, v reflect.Value) IndexedItem {
|
||||
str := v.String()
|
||||
return IndexedItem{
|
||||
Index: i,
|
||||
Value: str,
|
||||
Len: len(str),
|
||||
}
|
||||
})
|
||||
|
||||
result := mapper(reflectVal)
|
||||
assert.Len(t, result, 3)
|
||||
assert.Equal(t, 0, result[0].Index)
|
||||
assert.Equal(t, "apple", result[0].Value)
|
||||
assert.Equal(t, 5, result[0].Len)
|
||||
assert.Equal(t, 2, result[2].Index)
|
||||
assert.Equal(t, "cherry", result[2].Value)
|
||||
assert.Equal(t, 6, result[2].Len)
|
||||
}
|
||||
|
||||
// TestArray_ReduceWithIndex tests reducing an array (not slice)
|
||||
func TestArray_ReduceWithIndex(t *testing.T) {
|
||||
input := [3]int{10, 20, 30}
|
||||
reflectVal := reflect.ValueOf(input)
|
||||
|
||||
reducer := ReduceWithIndex(func(i int, acc int, v reflect.Value) int {
|
||||
return acc + i + int(v.Int())
|
||||
}, 0)
|
||||
|
||||
result := reducer(reflectVal)
|
||||
assert.Equal(t, 63, result, "Should work with arrays")
|
||||
}
|
||||
|
||||
// TestArray_Reduce tests reducing an array (not slice)
|
||||
func TestArray_Reduce(t *testing.T) {
|
||||
input := [4]int{1, 2, 3, 4}
|
||||
reflectVal := reflect.ValueOf(input)
|
||||
|
||||
reducer := Reduce(func(acc int, v reflect.Value) int {
|
||||
return acc + int(v.Int())
|
||||
}, 0)
|
||||
|
||||
result := reducer(reflectVal)
|
||||
assert.Equal(t, 10, result, "Should work with arrays")
|
||||
}
|
||||
|
||||
// TestArray_Map tests mapping an array (not slice)
|
||||
func TestArray_Map(t *testing.T) {
|
||||
input := [3]int{1, 2, 3}
|
||||
reflectVal := reflect.ValueOf(input)
|
||||
|
||||
mapper := Map(func(v reflect.Value) int {
|
||||
return int(v.Int()) * 2
|
||||
})
|
||||
|
||||
result := mapper(reflectVal)
|
||||
expected := []int{2, 4, 6}
|
||||
assert.Equal(t, expected, result, "Should work with arrays")
|
||||
}
|
||||
|
||||
// TestArray_MapWithIndex tests mapping an array with index
|
||||
func TestArray_MapWithIndex(t *testing.T) {
|
||||
input := [3]string{"a", "b", "c"}
|
||||
reflectVal := reflect.ValueOf(input)
|
||||
|
||||
mapper := MapWithIndex(func(i int, v reflect.Value) string {
|
||||
return string(rune('0'+i)) + v.String()
|
||||
})
|
||||
|
||||
result := mapper(reflectVal)
|
||||
expected := []string{"0a", "1b", "2c"}
|
||||
assert.Equal(t, expected, result, "Should work with arrays")
|
||||
}
|
||||
|
||||
// TestString_ReduceWithIndex tests reducing a string
|
||||
func TestString_ReduceWithIndex(t *testing.T) {
|
||||
input := "abc"
|
||||
reflectVal := reflect.ValueOf(input)
|
||||
|
||||
// Concatenate characters with their indices
|
||||
reducer := ReduceWithIndex(func(i int, acc string, v reflect.Value) string {
|
||||
char := byte(v.Uint()) // String index returns uint8
|
||||
if acc == "" {
|
||||
return string(rune('0'+i)) + string(char)
|
||||
}
|
||||
return acc + "," + string(rune('0'+i)) + string(char)
|
||||
}, "")
|
||||
|
||||
result := reducer(reflectVal)
|
||||
assert.Equal(t, "0a,1b,2c", result, "Should work with strings")
|
||||
}
|
||||
|
||||
// TestString_Reduce tests reducing a string
|
||||
func TestString_Reduce(t *testing.T) {
|
||||
input := "hello"
|
||||
reflectVal := reflect.ValueOf(input)
|
||||
|
||||
// Count characters
|
||||
reducer := Reduce(func(acc int, v reflect.Value) int {
|
||||
return acc + 1
|
||||
}, 0)
|
||||
|
||||
result := reducer(reflectVal)
|
||||
assert.Equal(t, 5, result, "Should work with strings")
|
||||
}
|
||||
|
||||
// TestString_Map tests mapping a string
|
||||
func TestString_Map(t *testing.T) {
|
||||
input := "abc"
|
||||
reflectVal := reflect.ValueOf(input)
|
||||
|
||||
// Convert to uppercase ASCII codes
|
||||
mapper := Map(func(v reflect.Value) int {
|
||||
return int(v.Uint()) - 32 // Convert lowercase to uppercase (uint8 for string bytes)
|
||||
})
|
||||
|
||||
result := mapper(reflectVal)
|
||||
expected := []int{65, 66, 67} // 'A', 'B', 'C'
|
||||
assert.Equal(t, expected, result, "Should work with strings")
|
||||
}
|
||||
|
||||
// TestString_MapWithIndex tests mapping a string with index
|
||||
func TestString_MapWithIndex(t *testing.T) {
|
||||
input := "xyz"
|
||||
reflectVal := reflect.ValueOf(input)
|
||||
|
||||
mapper := MapWithIndex(func(i int, v reflect.Value) string {
|
||||
char := byte(v.Uint()) // String index returns uint8
|
||||
return string(rune('0'+i)) + ":" + string(char)
|
||||
})
|
||||
|
||||
result := mapper(reflectVal)
|
||||
expected := []string{"0:x", "1:y", "2:z"}
|
||||
assert.Equal(t, expected, result, "Should work with strings")
|
||||
}
|
||||
|
||||
// TestNonIterable_ReduceWithIndex tests reducing a non-iterable type
|
||||
func TestNonIterable_ReduceWithIndex(t *testing.T) {
|
||||
input := 42
|
||||
reflectVal := reflect.ValueOf(input)
|
||||
|
||||
reducer := ReduceWithIndex(func(i int, acc int, v reflect.Value) int {
|
||||
return acc + int(v.Int())
|
||||
}, 100)
|
||||
|
||||
result := reducer(reflectVal)
|
||||
assert.Equal(t, 100, result, "Should return initial value for non-iterable")
|
||||
}
|
||||
|
||||
// TestNonIterable_Reduce tests reducing a non-iterable type
|
||||
func TestNonIterable_Reduce(t *testing.T) {
|
||||
// Try to reduce a struct (wrong kind)
|
||||
type MyStruct struct {
|
||||
Field int
|
||||
}
|
||||
structVal := reflect.ValueOf(MyStruct{Field: 10})
|
||||
|
||||
reducer := Reduce(func(acc int, v reflect.Value) int {
|
||||
return acc + 1
|
||||
}, 50)
|
||||
|
||||
result := reducer(structVal)
|
||||
assert.Equal(t, 50, result, "Should return initial value for struct")
|
||||
}
|
||||
|
||||
// TestNonIterable_Map tests mapping a non-iterable type
|
||||
func TestNonIterable_Map(t *testing.T) {
|
||||
input := 3.14
|
||||
reflectVal := reflect.ValueOf(input)
|
||||
|
||||
mapper := Map(func(v reflect.Value) int {
|
||||
return int(v.Float())
|
||||
})
|
||||
|
||||
result := mapper(reflectVal)
|
||||
assert.Empty(t, result, "Should return empty slice for non-iterable")
|
||||
assert.NotNil(t, result, "Should not return nil")
|
||||
}
|
||||
|
||||
// TestNonIterable_MapWithIndex tests mapping a non-iterable type with index
|
||||
func TestNonIterable_MapWithIndex(t *testing.T) {
|
||||
type MyStruct struct {
|
||||
Value int
|
||||
}
|
||||
input := MyStruct{Value: 42}
|
||||
reflectVal := reflect.ValueOf(input)
|
||||
|
||||
mapper := MapWithIndex(func(i int, v reflect.Value) int {
|
||||
return i
|
||||
})
|
||||
|
||||
result := mapper(reflectVal)
|
||||
assert.Empty(t, result, "Should return empty slice for non-iterable")
|
||||
assert.NotNil(t, result, "Should not return nil")
|
||||
}
|
||||
|
||||
// TestNonIterable_Map tests mapping a map type (not supported)
|
||||
func TestNonIterable_MapType(t *testing.T) {
|
||||
input := map[string]int{"a": 1, "b": 2}
|
||||
reflectVal := reflect.ValueOf(input)
|
||||
|
||||
mapper := Map(func(v reflect.Value) int {
|
||||
return 0
|
||||
})
|
||||
|
||||
result := mapper(reflectVal)
|
||||
assert.Empty(t, result, "Should return empty slice for map type")
|
||||
assert.NotNil(t, result, "Should not return nil")
|
||||
}
|
||||
|
||||
// TestNonIterable_Channel tests with channel type (not supported)
|
||||
func TestNonIterable_Channel(t *testing.T) {
|
||||
input := make(chan int)
|
||||
reflectVal := reflect.ValueOf(input)
|
||||
|
||||
reducer := Reduce(func(acc int, v reflect.Value) int {
|
||||
return acc + 1
|
||||
}, 99)
|
||||
|
||||
result := reducer(reflectVal)
|
||||
assert.Equal(t, 99, result, "Should return initial value for channel")
|
||||
}
|
||||
|
||||
// TestEdgeCase_NilSlice tests with nil slice
|
||||
func TestEdgeCase_NilSlice(t *testing.T) {
|
||||
var input []int
|
||||
reflectVal := reflect.ValueOf(input)
|
||||
|
||||
mapper := Map(func(v reflect.Value) int {
|
||||
return int(v.Int()) * 2
|
||||
})
|
||||
|
||||
result := mapper(reflectVal)
|
||||
assert.Empty(t, result, "Should return empty slice for nil slice")
|
||||
}
|
||||
|
||||
// TestEdgeCase_LargeSlice tests with a larger slice
|
||||
func TestEdgeCase_LargeSlice(t *testing.T) {
|
||||
input := make([]int, 1000)
|
||||
for i := range input {
|
||||
input[i] = i
|
||||
}
|
||||
reflectVal := reflect.ValueOf(input)
|
||||
|
||||
reducer := Reduce(func(acc int, v reflect.Value) int {
|
||||
return acc + int(v.Int())
|
||||
}, 0)
|
||||
|
||||
result := reducer(reflectVal)
|
||||
// Sum of 0 to 999 = 999 * 1000 / 2 = 499500
|
||||
assert.Equal(t, 499500, result, "Should handle large slices")
|
||||
}
|
||||
|
||||
// TestMonadMapWithIndex_IntToString tests the non-curried version
|
||||
func TestMonadMapWithIndex_IntToString(t *testing.T) {
|
||||
input := []int{10, 20, 30}
|
||||
reflectVal := reflect.ValueOf(input)
|
||||
|
||||
result := MonadMapWithIndex(reflectVal, func(i int, v reflect.Value) string {
|
||||
return string(rune('A'+i)) + ":" + string(rune('0'+int(v.Int()/10)))
|
||||
})
|
||||
|
||||
expected := []string{"A:1", "B:2", "C:3"}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
// TestMonadMapWithIndex_WithCalculation tests using index in calculation
|
||||
func TestMonadMapWithIndex_WithCalculation(t *testing.T) {
|
||||
input := []int{5, 10, 15}
|
||||
reflectVal := reflect.ValueOf(input)
|
||||
|
||||
result := MonadMapWithIndex(reflectVal, func(i int, v reflect.Value) int {
|
||||
return i * int(v.Int())
|
||||
})
|
||||
|
||||
expected := []int{0, 10, 30} // 0*5, 1*10, 2*15
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
// TestMonadMapWithIndex_EmptySlice tests with empty slice
|
||||
func TestMonadMapWithIndex_EmptySlice(t *testing.T) {
|
||||
input := []int{}
|
||||
reflectVal := reflect.ValueOf(input)
|
||||
|
||||
result := MonadMapWithIndex(reflectVal, func(i int, v reflect.Value) int {
|
||||
return i + int(v.Int())
|
||||
})
|
||||
|
||||
assert.Empty(t, result, "Should return empty slice")
|
||||
assert.NotNil(t, result, "Should not return nil")
|
||||
}
|
||||
|
||||
// TestMonadMapWithIndex_Array tests with array type
|
||||
func TestMonadMapWithIndex_Array(t *testing.T) {
|
||||
input := [3]string{"a", "b", "c"}
|
||||
reflectVal := reflect.ValueOf(input)
|
||||
|
||||
result := MonadMapWithIndex(reflectVal, func(i int, v reflect.Value) string {
|
||||
return string(rune('0'+i)) + v.String()
|
||||
})
|
||||
|
||||
expected := []string{"0a", "1b", "2c"}
|
||||
assert.Equal(t, expected, result, "Should work with arrays")
|
||||
}
|
||||
|
||||
// TestMonadMapWithIndex_String tests with string type
|
||||
func TestMonadMapWithIndex_String(t *testing.T) {
|
||||
input := "xyz"
|
||||
reflectVal := reflect.ValueOf(input)
|
||||
|
||||
result := MonadMapWithIndex(reflectVal, func(i int, v reflect.Value) string {
|
||||
char := byte(v.Uint()) // String index returns uint8
|
||||
return string(rune('0'+i)) + ":" + string(char)
|
||||
})
|
||||
|
||||
expected := []string{"0:x", "1:y", "2:z"}
|
||||
assert.Equal(t, expected, result, "Should work with strings")
|
||||
}
|
||||
|
||||
// TestMonadMapWithIndex_NonIterable tests with non-iterable type
|
||||
func TestMonadMapWithIndex_NonIterable(t *testing.T) {
|
||||
type MyStruct struct {
|
||||
Value int
|
||||
}
|
||||
input := MyStruct{Value: 42}
|
||||
reflectVal := reflect.ValueOf(input)
|
||||
|
||||
result := MonadMapWithIndex(reflectVal, func(i int, v reflect.Value) int {
|
||||
return i
|
||||
})
|
||||
|
||||
assert.Empty(t, result, "Should return empty slice for non-iterable")
|
||||
assert.NotNil(t, result, "Should not return nil")
|
||||
}
|
||||
|
||||
// TestMonadMapWithIndex_ComplexTransformation tests complex transformation
|
||||
func TestMonadMapWithIndex_ComplexTransformation(t *testing.T) {
|
||||
input := []string{"apple", "banana", "cherry"}
|
||||
reflectVal := reflect.ValueOf(input)
|
||||
|
||||
type IndexedItem struct {
|
||||
Index int
|
||||
Value string
|
||||
Len int
|
||||
}
|
||||
|
||||
result := MonadMapWithIndex(reflectVal, func(i int, v reflect.Value) IndexedItem {
|
||||
str := v.String()
|
||||
return IndexedItem{
|
||||
Index: i,
|
||||
Value: str,
|
||||
Len: len(str),
|
||||
}
|
||||
})
|
||||
|
||||
assert.Len(t, result, 3)
|
||||
assert.Equal(t, 0, result[0].Index)
|
||||
assert.Equal(t, "apple", result[0].Value)
|
||||
assert.Equal(t, 5, result[0].Len)
|
||||
assert.Equal(t, 2, result[2].Index)
|
||||
assert.Equal(t, "cherry", result[2].Value)
|
||||
assert.Equal(t, 6, result[2].Len)
|
||||
}
|
||||
|
||||
// TestMonadMapWithIndex_NilSlice tests with nil slice
|
||||
func TestMonadMapWithIndex_NilSlice(t *testing.T) {
|
||||
var input []int
|
||||
reflectVal := reflect.ValueOf(input)
|
||||
|
||||
result := MonadMapWithIndex(reflectVal, func(i int, v reflect.Value) int {
|
||||
return int(v.Int()) * 2
|
||||
})
|
||||
|
||||
assert.Empty(t, result, "Should return empty slice for nil slice")
|
||||
}
|
||||
|
||||
@@ -564,3 +564,42 @@ func Flap[B, A any](a A) Operator[func(A) B, B] {
|
||||
func MonadAlt[A any](fa Result[A], that Lazy[Result[A]]) Result[A] {
|
||||
return either.MonadAlt(fa, that)
|
||||
}
|
||||
|
||||
// Zero returns the zero value of a [Result], which is a Right containing the zero value of type A.
|
||||
// This function is useful as an identity element in monoid operations or for creating an empty Result
|
||||
// in a successful (Right) state.
|
||||
//
|
||||
// Result[A] is an alias for Either[error, A], so Zero returns a Right value with the zero value of type A.
|
||||
// For reference types (pointers, slices, maps, channels, functions, interfaces), the zero value is nil.
|
||||
// For value types (numbers, booleans, structs), it's the type's zero value.
|
||||
//
|
||||
// Important: Zero() returns the same value as the default initialization of Result[A].
|
||||
// When you declare `var r Result[A]` without initialization, it has the same value as Zero[A]().
|
||||
//
|
||||
// Note: This always produces a successful (Right) state with a zero value, never a Left (error) state.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Zero Result with int value
|
||||
// r1 := result.Zero[int]() // Right(0)
|
||||
//
|
||||
// // Zero Result with string value
|
||||
// r2 := result.Zero[string]() // Right("")
|
||||
//
|
||||
// // Zero Result with pointer type
|
||||
// r3 := result.Zero[*int]() // Right(nil)
|
||||
//
|
||||
// // Zero equals default initialization
|
||||
// var defaultInit Result[int]
|
||||
// zero := result.Zero[int]()
|
||||
// assert.Equal(t, defaultInit, zero) // true
|
||||
//
|
||||
// // Verify it's a Right value
|
||||
// r := result.Zero[int]()
|
||||
// assert.True(t, either.IsRight(r)) // true
|
||||
// assert.False(t, either.IsLeft(r)) // false
|
||||
//
|
||||
//go:inline
|
||||
func Zero[A any]() Result[A] {
|
||||
return either.Zero[error, A]()
|
||||
}
|
||||
|
||||
@@ -169,3 +169,17 @@ func TestOrElse(t *testing.T) {
|
||||
otherErr := errors.New("other error")
|
||||
assert.Equal(t, Left[int](otherErr), recoverSpecific(Left[int](otherErr)))
|
||||
}
|
||||
|
||||
// TestZeroEqualsDefaultInitialization tests that Zero returns the same value as default initialization
|
||||
func TestZeroEqualsDefaultInitialization(t *testing.T) {
|
||||
// Default initialization of Result
|
||||
var defaultInit Result[int]
|
||||
|
||||
// Zero function
|
||||
zero := Zero[int]()
|
||||
|
||||
// They should be equal
|
||||
assert.Equal(t, defaultInit, zero, "Zero should equal default initialization")
|
||||
assert.Equal(t, IsRight(defaultInit), IsRight(zero), "Both should be Right")
|
||||
assert.Equal(t, IsLeft(defaultInit), IsLeft(zero), "Both should not be Left")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user