mirror of
https://github.com/IBM/fp-go.git
synced 2026-01-15 00:53:10 +02:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
840ffbb51d | ||
|
|
380ba2853c | ||
|
|
c18e5e2107 | ||
|
|
89766bdb26 | ||
|
|
21d116d325 |
@@ -622,3 +622,128 @@ func Prepend[A any](head A) Operator[A, A] {
|
||||
func Reverse[A any](as []A) []A {
|
||||
return G.Reverse(as)
|
||||
}
|
||||
|
||||
// Extend applies a function to every suffix of an array, creating a new array of results.
|
||||
// This is the comonad extend operation for arrays.
|
||||
//
|
||||
// The function f is applied to progressively smaller suffixes of the input array:
|
||||
// - f(as[0:]) for the first element
|
||||
// - f(as[1:]) for the second element
|
||||
// - f(as[2:]) for the third element
|
||||
// - and so on...
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of elements in the input array
|
||||
// - B: The type of elements in the output array
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that takes an array suffix and returns a value
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms an array of A into an array of B
|
||||
//
|
||||
// Behavior:
|
||||
// - Creates a new array with the same length as the input
|
||||
// - For each position i, applies f to the suffix starting at i
|
||||
// - Returns an empty array if the input is empty
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Sum all elements from current position to end
|
||||
// sumSuffix := array.Extend(func(as []int) int {
|
||||
// return array.Reduce(func(acc, x int) int { return acc + x }, 0)(as)
|
||||
// })
|
||||
// result := sumSuffix([]int{1, 2, 3, 4})
|
||||
// // result: []int{10, 9, 7, 4}
|
||||
// // Explanation: [1+2+3+4, 2+3+4, 3+4, 4]
|
||||
//
|
||||
// Example with length:
|
||||
//
|
||||
// // Get remaining length at each position
|
||||
// lengths := array.Extend(array.Size[int])
|
||||
// result := lengths([]int{10, 20, 30})
|
||||
// // result: []int{3, 2, 1}
|
||||
//
|
||||
// Example with head:
|
||||
//
|
||||
// // Duplicate each element (extract head of each suffix)
|
||||
// duplicate := array.Extend(func(as []int) int {
|
||||
// return F.Pipe1(as, array.Head[int], O.GetOrElse(F.Constant(0)))
|
||||
// })
|
||||
// result := duplicate([]int{1, 2, 3})
|
||||
// // result: []int{1, 2, 3}
|
||||
//
|
||||
// Use cases:
|
||||
// - Computing cumulative or rolling operations
|
||||
// - Implementing sliding window algorithms
|
||||
// - Creating context-aware transformations
|
||||
// - Building comonadic computations
|
||||
//
|
||||
// Comonad laws:
|
||||
// - Left identity: Extend(Extract) == Identity
|
||||
// - Right identity: Extract ∘ Extend(f) == f
|
||||
// - Associativity: Extend(f) ∘ Extend(g) == Extend(f ∘ Extend(g))
|
||||
//
|
||||
//go:inline
|
||||
func Extend[A, B any](f func([]A) B) Operator[A, B] {
|
||||
return func(as []A) []B {
|
||||
return G.MakeBy[[]B](len(as), func(i int) B { return f(as[i:]) })
|
||||
}
|
||||
}
|
||||
|
||||
// Extract returns the first element of an array, or a zero value if empty.
|
||||
// This is the comonad extract operation for arrays.
|
||||
//
|
||||
// Extract is the dual of the monadic return/of operation. While Of wraps a value
|
||||
// in a context, Extract unwraps a value from its context.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of elements in the array
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The input array
|
||||
//
|
||||
// Returns:
|
||||
// - The first element if the array is non-empty, otherwise the zero value of type A
|
||||
//
|
||||
// Behavior:
|
||||
// - Returns as[0] if the array has at least one element
|
||||
// - Returns the zero value of A if the array is empty
|
||||
// - Does not modify the input array
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := array.Extract([]int{1, 2, 3})
|
||||
// // result: 1
|
||||
//
|
||||
// Example with empty array:
|
||||
//
|
||||
// result := array.Extract([]int{})
|
||||
// // result: 0 (zero value for int)
|
||||
//
|
||||
// Example with strings:
|
||||
//
|
||||
// result := array.Extract([]string{"hello", "world"})
|
||||
// // result: "hello"
|
||||
//
|
||||
// Example with empty string array:
|
||||
//
|
||||
// result := array.Extract([]string{})
|
||||
// // result: "" (zero value for string)
|
||||
//
|
||||
// Use cases:
|
||||
// - Extracting the current focus from a comonadic context
|
||||
// - Getting the head element with a default zero value
|
||||
// - Implementing comonad-based computations
|
||||
//
|
||||
// Comonad laws:
|
||||
// - Extract ∘ Of == Identity (extracting from a singleton returns the value)
|
||||
// - Extract ∘ Extend(f) == f (extract after extend equals applying f)
|
||||
//
|
||||
// Note: For a safer alternative that handles empty arrays explicitly,
|
||||
// consider using Head which returns an Option[A].
|
||||
//
|
||||
//go:inline
|
||||
func Extract[A any](as []A) A {
|
||||
return G.Extract(as)
|
||||
}
|
||||
|
||||
@@ -474,3 +474,293 @@ func TestReverseProperties(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestExtract tests the Extract function
|
||||
func TestExtract(t *testing.T) {
|
||||
t.Run("Extract from non-empty array", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
result := Extract(input)
|
||||
assert.Equal(t, 1, result)
|
||||
})
|
||||
|
||||
t.Run("Extract from single element array", func(t *testing.T) {
|
||||
input := []string{"hello"}
|
||||
result := Extract(input)
|
||||
assert.Equal(t, "hello", result)
|
||||
})
|
||||
|
||||
t.Run("Extract from empty array returns zero value", func(t *testing.T) {
|
||||
input := []int{}
|
||||
result := Extract(input)
|
||||
assert.Equal(t, 0, result)
|
||||
})
|
||||
|
||||
t.Run("Extract from empty string array returns empty string", func(t *testing.T) {
|
||||
input := []string{}
|
||||
result := Extract(input)
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
|
||||
t.Run("Extract does not modify original array", func(t *testing.T) {
|
||||
original := []int{1, 2, 3}
|
||||
originalCopy := []int{1, 2, 3}
|
||||
_ = Extract(original)
|
||||
assert.Equal(t, originalCopy, original)
|
||||
})
|
||||
|
||||
t.Run("Extract with floats", func(t *testing.T) {
|
||||
input := []float64{3.14, 2.71, 1.41}
|
||||
result := Extract(input)
|
||||
assert.Equal(t, 3.14, result)
|
||||
})
|
||||
|
||||
t.Run("Extract with structs", func(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
input := []Person{
|
||||
{"Alice", 30},
|
||||
{"Bob", 25},
|
||||
}
|
||||
result := Extract(input)
|
||||
assert.Equal(t, Person{"Alice", 30}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestExtractComonadLaws tests comonad laws for Extract
|
||||
func TestExtractComonadLaws(t *testing.T) {
|
||||
t.Run("Extract ∘ Of == Identity", func(t *testing.T) {
|
||||
value := 42
|
||||
result := Extract(Of(value))
|
||||
assert.Equal(t, value, result)
|
||||
})
|
||||
|
||||
t.Run("Extract ∘ Extend(f) == f", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4}
|
||||
f := func(as []int) int {
|
||||
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
|
||||
}
|
||||
|
||||
// Extract(Extend(f)(input)) should equal f(input)
|
||||
extended := Extend(f)(input)
|
||||
result := Extract(extended)
|
||||
expected := f(input)
|
||||
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestExtend tests the Extend function
|
||||
func TestExtend(t *testing.T) {
|
||||
t.Run("Extend with sum of suffixes", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4}
|
||||
sumSuffix := Extend(func(as []int) int {
|
||||
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
|
||||
})
|
||||
result := sumSuffix(input)
|
||||
expected := []int{10, 9, 7, 4} // [1+2+3+4, 2+3+4, 3+4, 4]
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Extend with length of suffixes", func(t *testing.T) {
|
||||
input := []int{10, 20, 30}
|
||||
lengths := Extend(Size[int])
|
||||
result := lengths(input)
|
||||
expected := []int{3, 2, 1}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Extend with head extraction", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
duplicate := Extend(func(as []int) int {
|
||||
return F.Pipe2(as, Head[int], O.GetOrElse(F.Constant(0)))
|
||||
})
|
||||
result := duplicate(input)
|
||||
expected := []int{1, 2, 3}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Extend with empty array", func(t *testing.T) {
|
||||
input := []int{}
|
||||
result := Extend(Size[int])(input)
|
||||
assert.Equal(t, []int{}, result)
|
||||
})
|
||||
|
||||
t.Run("Extend with single element", func(t *testing.T) {
|
||||
input := []string{"hello"}
|
||||
result := Extend(func(as []string) int { return len(as) })(input)
|
||||
expected := []int{1}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Extend does not modify original array", func(t *testing.T) {
|
||||
original := []int{1, 2, 3}
|
||||
originalCopy := []int{1, 2, 3}
|
||||
_ = Extend(Size[int])(original)
|
||||
assert.Equal(t, originalCopy, original)
|
||||
})
|
||||
|
||||
t.Run("Extend with string concatenation", func(t *testing.T) {
|
||||
input := []string{"a", "b", "c"}
|
||||
concat := Extend(func(as []string) string {
|
||||
return MonadReduce(as, func(acc, s string) string { return acc + s }, "")
|
||||
})
|
||||
result := concat(input)
|
||||
expected := []string{"abc", "bc", "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Extend with max of suffixes", func(t *testing.T) {
|
||||
input := []int{3, 1, 4, 1, 5}
|
||||
maxSuffix := Extend(func(as []int) int {
|
||||
if len(as) == 0 {
|
||||
return 0
|
||||
}
|
||||
max := as[0]
|
||||
for _, v := range as[1:] {
|
||||
if v > max {
|
||||
max = v
|
||||
}
|
||||
}
|
||||
return max
|
||||
})
|
||||
result := maxSuffix(input)
|
||||
expected := []int{5, 5, 5, 5, 5}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestExtendComonadLaws tests comonad laws for Extend
|
||||
func TestExtendComonadLaws(t *testing.T) {
|
||||
t.Run("Left identity: Extend(Extract) == Identity", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
result := Extend(Extract[int])(input)
|
||||
assert.Equal(t, input, result)
|
||||
})
|
||||
|
||||
t.Run("Right identity: Extract ∘ Extend(f) == f", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4}
|
||||
f := func(as []int) int {
|
||||
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
|
||||
}
|
||||
|
||||
// Extract(Extend(f)(input)) should equal f(input)
|
||||
result := F.Pipe2(input, Extend(f), Extract[int])
|
||||
expected := f(input)
|
||||
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Associativity: Extend(f) ∘ Extend(g) == Extend(f ∘ Extend(g))", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
|
||||
// f: sum of array
|
||||
f := func(as []int) int {
|
||||
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
|
||||
}
|
||||
|
||||
// g: length of array
|
||||
g := func(as []int) int {
|
||||
return len(as)
|
||||
}
|
||||
|
||||
// Left side: Extend(f) ∘ Extend(g)
|
||||
left := F.Pipe2(input, Extend(g), Extend(f))
|
||||
|
||||
// Right side: Extend(f ∘ Extend(g))
|
||||
right := Extend(func(as []int) int {
|
||||
return f(Extend(g)(as))
|
||||
})(input)
|
||||
|
||||
assert.Equal(t, left, right)
|
||||
})
|
||||
}
|
||||
|
||||
// TestExtendComposition tests Extend with other array operations
|
||||
func TestExtendComposition(t *testing.T) {
|
||||
t.Run("Extend after Map", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
result := F.Pipe2(
|
||||
input,
|
||||
Map(N.Mul(2)),
|
||||
Extend(func(as []int) int {
|
||||
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
|
||||
}),
|
||||
)
|
||||
expected := []int{12, 10, 6} // [2+4+6, 4+6, 6]
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Map after Extend", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
result := F.Pipe2(
|
||||
input,
|
||||
Extend(Size[int]),
|
||||
Map(N.Mul(10)),
|
||||
)
|
||||
expected := []int{30, 20, 10}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Extend with Filter", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4, 5, 6}
|
||||
result := F.Pipe2(
|
||||
input,
|
||||
Filter(func(n int) bool { return n%2 == 0 }),
|
||||
Extend(Size[int]),
|
||||
)
|
||||
expected := []int{3, 2, 1} // lengths of [2,4,6], [4,6], [6]
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestExtendUseCases demonstrates practical use cases for Extend
|
||||
func TestExtendUseCases(t *testing.T) {
|
||||
t.Run("Running sum (cumulative sum from each position)", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
runningSum := Extend(func(as []int) int {
|
||||
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
|
||||
})
|
||||
result := runningSum(input)
|
||||
expected := []int{15, 14, 12, 9, 5}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Sliding window average", func(t *testing.T) {
|
||||
input := []float64{1.0, 2.0, 3.0, 4.0, 5.0}
|
||||
windowAvg := Extend(func(as []float64) float64 {
|
||||
if len(as) == 0 {
|
||||
return 0
|
||||
}
|
||||
sum := MonadReduce(as, func(acc, x float64) float64 { return acc + x }, 0.0)
|
||||
return sum / float64(len(as))
|
||||
})
|
||||
result := windowAvg(input)
|
||||
expected := []float64{3.0, 3.5, 4.0, 4.5, 5.0}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Check if suffix is sorted", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 2, 1}
|
||||
isSorted := Extend(func(as []int) bool {
|
||||
for i := 1; i < len(as); i++ {
|
||||
if as[i] < as[i-1] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
result := isSorted(input)
|
||||
expected := []bool{false, false, false, false, true}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Count remaining elements", func(t *testing.T) {
|
||||
events := []string{"start", "middle", "end"}
|
||||
remaining := Extend(Size[string])
|
||||
result := remaining(events)
|
||||
expected := []int{3, 2, 1}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -375,3 +375,102 @@ func Prepend[ENDO ~func(AS) AS, AS []A, A any](head A) ENDO {
|
||||
func Reverse[GT ~[]T, T any](as GT) GT {
|
||||
return array.Reverse(as)
|
||||
}
|
||||
|
||||
// Extract returns the first element of an array, or a zero value if empty.
|
||||
// This is the comonad extract operation for arrays.
|
||||
//
|
||||
// Extract is the dual of the monadic return/of operation. While Of wraps a value
|
||||
// in a context, Extract unwraps a value from its context.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: The array type constraint
|
||||
// - A: The type of elements in the array
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The input array
|
||||
//
|
||||
// Returns:
|
||||
// - The first element if the array is non-empty, otherwise the zero value of type A
|
||||
//
|
||||
// Behavior:
|
||||
// - Returns as[0] if the array has at least one element
|
||||
// - Returns the zero value of A if the array is empty
|
||||
// - Does not modify the input array
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := Extract([]int{1, 2, 3})
|
||||
// // result: 1
|
||||
//
|
||||
// Example with empty array:
|
||||
//
|
||||
// result := Extract([]int{})
|
||||
// // result: 0 (zero value for int)
|
||||
//
|
||||
// Comonad laws:
|
||||
// - Extract ∘ Of == Identity (extracting from a singleton returns the value)
|
||||
// - Extract ∘ Extend(f) == f (extract after extend equals applying f)
|
||||
//
|
||||
//go:inline
|
||||
func Extract[GA ~[]A, A any](as GA) A {
|
||||
if len(as) > 0 {
|
||||
return as[0]
|
||||
}
|
||||
var zero A
|
||||
return zero
|
||||
}
|
||||
|
||||
// Extend applies a function to every suffix of an array, creating a new array of results.
|
||||
// This is the comonad extend operation for arrays.
|
||||
//
|
||||
// The function f is applied to progressively smaller suffixes of the input array:
|
||||
// - f(as[0:]) for the first element
|
||||
// - f(as[1:]) for the second element
|
||||
// - f(as[2:]) for the third element
|
||||
// - and so on...
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: The input array type constraint
|
||||
// - GB: The output array type constraint
|
||||
// - A: The type of elements in the input array
|
||||
// - B: The type of elements in the output array
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that takes an array suffix and returns a value
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms an array of A into an array of B
|
||||
//
|
||||
// Behavior:
|
||||
// - Creates a new array with the same length as the input
|
||||
// - For each position i, applies f to the suffix starting at i
|
||||
// - Returns an empty array if the input is empty
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Sum all elements from current position to end
|
||||
// sumSuffix := Extend[[]int, []int](func(as []int) int {
|
||||
// return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
|
||||
// })
|
||||
// result := sumSuffix([]int{1, 2, 3, 4})
|
||||
// // result: []int{10, 9, 7, 4}
|
||||
// // Explanation: [1+2+3+4, 2+3+4, 3+4, 4]
|
||||
//
|
||||
// Example with length:
|
||||
//
|
||||
// // Get remaining length at each position
|
||||
// lengths := Extend[[]int, []int](Size[[]int, int])
|
||||
// result := lengths([]int{10, 20, 30})
|
||||
// // result: []int{3, 2, 1}
|
||||
//
|
||||
// Comonad laws:
|
||||
// - Left identity: Extend(Extract) == Identity
|
||||
// - Right identity: Extract ∘ Extend(f) == f
|
||||
// - Associativity: Extend(f) ∘ Extend(g) == Extend(f ∘ Extend(g))
|
||||
//
|
||||
//go:inline
|
||||
func Extend[GA ~[]A, GB ~[]B, A, B any](f func(GA) B) func(GA) GB {
|
||||
return func(as GA) GB {
|
||||
return MakeBy[GB](len(as), func(i int) B { return f(as[i:]) })
|
||||
}
|
||||
}
|
||||
|
||||
298
v2/array/generic/array_test.go
Normal file
298
v2/array/generic/array_test.go
Normal file
@@ -0,0 +1,298 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package generic
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestExtract tests the Extract function
|
||||
func TestExtract(t *testing.T) {
|
||||
t.Run("Extract from non-empty array", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
result := Extract(input)
|
||||
assert.Equal(t, 1, result)
|
||||
})
|
||||
|
||||
t.Run("Extract from single element array", func(t *testing.T) {
|
||||
input := []string{"hello"}
|
||||
result := Extract(input)
|
||||
assert.Equal(t, "hello", result)
|
||||
})
|
||||
|
||||
t.Run("Extract from empty array returns zero value", func(t *testing.T) {
|
||||
input := []int{}
|
||||
result := Extract(input)
|
||||
assert.Equal(t, 0, result)
|
||||
})
|
||||
|
||||
t.Run("Extract from empty string array returns empty string", func(t *testing.T) {
|
||||
input := []string{}
|
||||
result := Extract(input)
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
|
||||
t.Run("Extract does not modify original array", func(t *testing.T) {
|
||||
original := []int{1, 2, 3}
|
||||
originalCopy := []int{1, 2, 3}
|
||||
_ = Extract(original)
|
||||
assert.Equal(t, originalCopy, original)
|
||||
})
|
||||
|
||||
t.Run("Extract with floats", func(t *testing.T) {
|
||||
input := []float64{3.14, 2.71, 1.41}
|
||||
result := Extract(input)
|
||||
assert.Equal(t, 3.14, result)
|
||||
})
|
||||
|
||||
t.Run("Extract with custom slice type", func(t *testing.T) {
|
||||
type IntSlice []int
|
||||
input := IntSlice{10, 20, 30}
|
||||
result := Extract(input)
|
||||
assert.Equal(t, 10, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestExtractComonadLaws tests comonad laws for Extract
|
||||
func TestExtractComonadLaws(t *testing.T) {
|
||||
t.Run("Extract ∘ Of == Identity", func(t *testing.T) {
|
||||
value := 42
|
||||
result := Extract(Of[[]int](value))
|
||||
assert.Equal(t, value, result)
|
||||
})
|
||||
|
||||
t.Run("Extract ∘ Extend(f) == f", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4}
|
||||
f := func(as []int) int {
|
||||
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
|
||||
}
|
||||
|
||||
// Extract(Extend(f)(input)) should equal f(input)
|
||||
extended := Extend[[]int, []int](f)(input)
|
||||
result := Extract(extended)
|
||||
expected := f(input)
|
||||
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestExtend tests the Extend function
|
||||
func TestExtend(t *testing.T) {
|
||||
t.Run("Extend with sum of suffixes", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4}
|
||||
sumSuffix := Extend[[]int, []int](func(as []int) int {
|
||||
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
|
||||
})
|
||||
result := sumSuffix(input)
|
||||
expected := []int{10, 9, 7, 4} // [1+2+3+4, 2+3+4, 3+4, 4]
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Extend with length of suffixes", func(t *testing.T) {
|
||||
input := []int{10, 20, 30}
|
||||
lengths := Extend[[]int, []int](Size[[]int, int])
|
||||
result := lengths(input)
|
||||
expected := []int{3, 2, 1}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Extend with head extraction", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
duplicate := Extend[[]int, []int](Extract[[]int, int])
|
||||
result := duplicate(input)
|
||||
expected := []int{1, 2, 3}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Extend with empty array", func(t *testing.T) {
|
||||
input := []int{}
|
||||
result := Extend[[]int, []int](Size[[]int, int])(input)
|
||||
assert.Equal(t, []int{}, result)
|
||||
})
|
||||
|
||||
t.Run("Extend with single element", func(t *testing.T) {
|
||||
input := []string{"hello"}
|
||||
result := Extend[[]string, []int](func(as []string) int { return len(as) })(input)
|
||||
expected := []int{1}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Extend does not modify original array", func(t *testing.T) {
|
||||
original := []int{1, 2, 3}
|
||||
originalCopy := []int{1, 2, 3}
|
||||
_ = Extend[[]int, []int](Size[[]int, int])(original)
|
||||
assert.Equal(t, originalCopy, original)
|
||||
})
|
||||
|
||||
t.Run("Extend with string concatenation", func(t *testing.T) {
|
||||
input := []string{"a", "b", "c"}
|
||||
concat := Extend[[]string, []string](func(as []string) string {
|
||||
return MonadReduce(as, func(acc, s string) string { return acc + s }, "")
|
||||
})
|
||||
result := concat(input)
|
||||
expected := []string{"abc", "bc", "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Extend with custom slice types", func(t *testing.T) {
|
||||
type IntSlice []int
|
||||
type ResultSlice []int
|
||||
input := IntSlice{1, 2, 3}
|
||||
sumSuffix := Extend[IntSlice, ResultSlice](func(as IntSlice) int {
|
||||
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
|
||||
})
|
||||
result := sumSuffix(input)
|
||||
expected := ResultSlice{6, 5, 3}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestExtendComonadLaws tests comonad laws for Extend
|
||||
func TestExtendComonadLaws(t *testing.T) {
|
||||
t.Run("Left identity: Extend(Extract) == Identity", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
result := Extend[[]int, []int](Extract[[]int, int])(input)
|
||||
assert.Equal(t, input, result)
|
||||
})
|
||||
|
||||
t.Run("Right identity: Extract ∘ Extend(f) == f", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4}
|
||||
f := func(as []int) int {
|
||||
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
|
||||
}
|
||||
|
||||
// Extract(Extend(f)(input)) should equal f(input)
|
||||
result := F.Pipe2(input, Extend[[]int, []int](f), Extract[[]int, int])
|
||||
expected := f(input)
|
||||
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Associativity: Extend(f) ∘ Extend(g) == Extend(f ∘ Extend(g))", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
|
||||
// f: sum of array
|
||||
f := func(as []int) int {
|
||||
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
|
||||
}
|
||||
|
||||
// g: length of array
|
||||
g := func(as []int) int {
|
||||
return len(as)
|
||||
}
|
||||
|
||||
// Left side: Extend(f) ∘ Extend(g)
|
||||
left := F.Pipe2(input, Extend[[]int, []int](g), Extend[[]int, []int](f))
|
||||
|
||||
// Right side: Extend(f ∘ Extend(g))
|
||||
right := Extend[[]int, []int](func(as []int) int {
|
||||
return f(Extend[[]int, []int](g)(as))
|
||||
})(input)
|
||||
|
||||
assert.Equal(t, left, right)
|
||||
})
|
||||
}
|
||||
|
||||
// TestExtendComposition tests Extend with other array operations
|
||||
func TestExtendComposition(t *testing.T) {
|
||||
t.Run("Extend after Map", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
result := F.Pipe2(
|
||||
input,
|
||||
Map[[]int, []int](func(x int) int { return x * 2 }),
|
||||
Extend[[]int, []int](func(as []int) int {
|
||||
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
|
||||
}),
|
||||
)
|
||||
expected := []int{12, 10, 6} // [2+4+6, 4+6, 6]
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Map after Extend", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
result := F.Pipe2(
|
||||
input,
|
||||
Extend[[]int, []int](Size[[]int, int]),
|
||||
Map[[]int, []int](func(x int) int { return x * 10 }),
|
||||
)
|
||||
expected := []int{30, 20, 10}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Extend with Filter", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4, 5, 6}
|
||||
result := F.Pipe2(
|
||||
input,
|
||||
Filter[[]int](func(n int) bool { return n%2 == 0 }),
|
||||
Extend[[]int, []int](Size[[]int, int]),
|
||||
)
|
||||
expected := []int{3, 2, 1} // lengths of [2,4,6], [4,6], [6]
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestExtendUseCases demonstrates practical use cases for Extend
|
||||
func TestExtendUseCases(t *testing.T) {
|
||||
t.Run("Running sum (cumulative sum from each position)", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
runningSum := Extend[[]int, []int](func(as []int) int {
|
||||
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
|
||||
})
|
||||
result := runningSum(input)
|
||||
expected := []int{15, 14, 12, 9, 5}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Sliding window average", func(t *testing.T) {
|
||||
input := []float64{1.0, 2.0, 3.0, 4.0, 5.0}
|
||||
windowAvg := Extend[[]float64, []float64](func(as []float64) float64 {
|
||||
if len(as) == 0 {
|
||||
return 0
|
||||
}
|
||||
sum := MonadReduce(as, func(acc, x float64) float64 { return acc + x }, 0.0)
|
||||
return sum / float64(len(as))
|
||||
})
|
||||
result := windowAvg(input)
|
||||
expected := []float64{3.0, 3.5, 4.0, 4.5, 5.0}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Check if suffix is sorted", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 2, 1}
|
||||
isSorted := Extend[[]int, []bool](func(as []int) bool {
|
||||
for i := 1; i < len(as); i++ {
|
||||
if as[i] < as[i-1] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
result := isSorted(input)
|
||||
expected := []bool{false, false, false, false, true}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Count remaining elements", func(t *testing.T) {
|
||||
events := []string{"start", "middle", "end"}
|
||||
remaining := Extend[[]string, []int](Size[[]string, string])
|
||||
result := remaining(events)
|
||||
expected := []int{3, 2, 1}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
@@ -23,12 +23,45 @@ import (
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
)
|
||||
|
||||
// Of constructs a single element array
|
||||
// Of constructs a single element NonEmptyArray.
|
||||
// This is the simplest way to create a NonEmptyArray with exactly one element.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type
|
||||
//
|
||||
// Parameters:
|
||||
// - first: The single element to include in the array
|
||||
//
|
||||
// Returns:
|
||||
// - NonEmptyArray[A]: A NonEmptyArray containing only the provided element
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// arr := Of(42) // NonEmptyArray[int]{42}
|
||||
// str := Of("hello") // NonEmptyArray[string]{"hello"}
|
||||
func Of[A any](first A) NonEmptyArray[A] {
|
||||
return G.Of[NonEmptyArray[A]](first)
|
||||
}
|
||||
|
||||
// From constructs a [NonEmptyArray] from a set of variadic arguments
|
||||
// From constructs a NonEmptyArray from a set of variadic arguments.
|
||||
// The first argument is required to ensure the array is non-empty, and additional
|
||||
// elements can be provided as variadic arguments.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type
|
||||
//
|
||||
// Parameters:
|
||||
// - first: The first element (required to ensure non-emptiness)
|
||||
// - data: Additional elements (optional)
|
||||
//
|
||||
// Returns:
|
||||
// - NonEmptyArray[A]: A NonEmptyArray containing all provided elements
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// arr1 := From(1) // NonEmptyArray[int]{1}
|
||||
// arr2 := From(1, 2, 3) // NonEmptyArray[int]{1, 2, 3}
|
||||
// arr3 := From("a", "b", "c") // NonEmptyArray[string]{"a", "b", "c"}
|
||||
func From[A any](first A, data ...A) NonEmptyArray[A] {
|
||||
count := len(data)
|
||||
if count == 0 {
|
||||
@@ -41,79 +74,358 @@ func From[A any](first A, data ...A) NonEmptyArray[A] {
|
||||
return buffer
|
||||
}
|
||||
|
||||
// IsEmpty always returns false for NonEmptyArray since it's guaranteed to have at least one element.
|
||||
// This function exists for API consistency with regular arrays.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type
|
||||
//
|
||||
// Parameters:
|
||||
// - _: The NonEmptyArray (unused, as the result is always false)
|
||||
//
|
||||
// Returns:
|
||||
// - bool: Always false
|
||||
//
|
||||
//go:inline
|
||||
func IsEmpty[A any](_ NonEmptyArray[A]) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsNonEmpty always returns true for NonEmptyArray since it's guaranteed to have at least one element.
|
||||
// This function exists for API consistency with regular arrays.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type
|
||||
//
|
||||
// Parameters:
|
||||
// - _: The NonEmptyArray (unused, as the result is always true)
|
||||
//
|
||||
// Returns:
|
||||
// - bool: Always true
|
||||
//
|
||||
//go:inline
|
||||
func IsNonEmpty[A any](_ NonEmptyArray[A]) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// MonadMap applies a function to each element of a NonEmptyArray, returning a new NonEmptyArray with the results.
|
||||
// This is the monadic version of Map that takes the array as the first parameter.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The input NonEmptyArray
|
||||
// - f: The function to apply to each element
|
||||
//
|
||||
// Returns:
|
||||
// - NonEmptyArray[B]: A new NonEmptyArray with the transformed elements
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// arr := From(1, 2, 3)
|
||||
// doubled := MonadMap(arr, func(x int) int { return x * 2 }) // NonEmptyArray[int]{2, 4, 6}
|
||||
//
|
||||
//go:inline
|
||||
func MonadMap[A, B any](as NonEmptyArray[A], f func(a A) B) NonEmptyArray[B] {
|
||||
return G.MonadMap[NonEmptyArray[A], NonEmptyArray[B]](as, f)
|
||||
}
|
||||
|
||||
// Map applies a function to each element of a NonEmptyArray, returning a new NonEmptyArray with the results.
|
||||
// This is the curried version that returns a function.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: The function to apply to each element
|
||||
//
|
||||
// Returns:
|
||||
// - Operator[A, B]: A function that transforms NonEmptyArray[A] to NonEmptyArray[B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// double := Map(func(x int) int { return x * 2 })
|
||||
// result := double(From(1, 2, 3)) // NonEmptyArray[int]{2, 4, 6}
|
||||
//
|
||||
//go:inline
|
||||
func Map[A, B any](f func(a A) B) Operator[A, B] {
|
||||
return G.Map[NonEmptyArray[A], NonEmptyArray[B]](f)
|
||||
}
|
||||
|
||||
// Reduce applies a function to each element of a NonEmptyArray from left to right,
|
||||
// accumulating a result starting from an initial value.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type of the array
|
||||
// - B: The accumulator type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: The reducer function that takes (accumulator, element) and returns a new accumulator
|
||||
// - initial: The initial value for the accumulator
|
||||
//
|
||||
// Returns:
|
||||
// - func(NonEmptyArray[A]) B: A function that reduces the array to a single value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// sum := Reduce(func(acc int, x int) int { return acc + x }, 0)
|
||||
// result := sum(From(1, 2, 3, 4)) // 10
|
||||
//
|
||||
// concat := Reduce(func(acc string, x string) string { return acc + x }, "")
|
||||
// result := concat(From("a", "b", "c")) // "abc"
|
||||
func Reduce[A, B any](f func(B, A) B, initial B) func(NonEmptyArray[A]) B {
|
||||
return func(as NonEmptyArray[A]) B {
|
||||
return array.Reduce(as, f, initial)
|
||||
}
|
||||
}
|
||||
|
||||
// ReduceRight applies a function to each element of a NonEmptyArray from right to left,
|
||||
// accumulating a result starting from an initial value.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type of the array
|
||||
// - B: The accumulator type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: The reducer function that takes (element, accumulator) and returns a new accumulator
|
||||
// - initial: The initial value for the accumulator
|
||||
//
|
||||
// Returns:
|
||||
// - func(NonEmptyArray[A]) B: A function that reduces the array to a single value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// concat := ReduceRight(func(x string, acc string) string { return acc + x }, "")
|
||||
// result := concat(From("a", "b", "c")) // "cba"
|
||||
func ReduceRight[A, B any](f func(A, B) B, initial B) func(NonEmptyArray[A]) B {
|
||||
return func(as NonEmptyArray[A]) B {
|
||||
return array.ReduceRight(as, f, initial)
|
||||
}
|
||||
}
|
||||
|
||||
// Tail returns all elements of a NonEmptyArray except the first one.
|
||||
// Returns an empty slice if the array has only one element.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The input NonEmptyArray
|
||||
//
|
||||
// Returns:
|
||||
// - []A: A slice containing all elements except the first (may be empty)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// arr := From(1, 2, 3, 4)
|
||||
// tail := Tail(arr) // []int{2, 3, 4}
|
||||
//
|
||||
// single := From(1)
|
||||
// tail := Tail(single) // []int{}
|
||||
//
|
||||
//go:inline
|
||||
func Tail[A any](as NonEmptyArray[A]) []A {
|
||||
return as[1:]
|
||||
}
|
||||
|
||||
// Head returns the first element of a NonEmptyArray.
|
||||
// This operation is always safe since NonEmptyArray is guaranteed to have at least one element.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The input NonEmptyArray
|
||||
//
|
||||
// Returns:
|
||||
// - A: The first element
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// arr := From(1, 2, 3)
|
||||
// first := Head(arr) // 1
|
||||
//
|
||||
//go:inline
|
||||
func Head[A any](as NonEmptyArray[A]) A {
|
||||
return as[0]
|
||||
}
|
||||
|
||||
// First returns the first element of a NonEmptyArray.
|
||||
// This is an alias for Head.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The input NonEmptyArray
|
||||
//
|
||||
// Returns:
|
||||
// - A: The first element
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// arr := From(1, 2, 3)
|
||||
// first := First(arr) // 1
|
||||
//
|
||||
//go:inline
|
||||
func First[A any](as NonEmptyArray[A]) A {
|
||||
return as[0]
|
||||
}
|
||||
|
||||
// Last returns the last element of a NonEmptyArray.
|
||||
// This operation is always safe since NonEmptyArray is guaranteed to have at least one element.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The input NonEmptyArray
|
||||
//
|
||||
// Returns:
|
||||
// - A: The last element
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// arr := From(1, 2, 3)
|
||||
// last := Last(arr) // 3
|
||||
//
|
||||
//go:inline
|
||||
func Last[A any](as NonEmptyArray[A]) A {
|
||||
return as[len(as)-1]
|
||||
}
|
||||
|
||||
// Size returns the number of elements in a NonEmptyArray.
|
||||
// The result is always at least 1.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The input NonEmptyArray
|
||||
//
|
||||
// Returns:
|
||||
// - int: The number of elements (always >= 1)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// arr := From(1, 2, 3)
|
||||
// size := Size(arr) // 3
|
||||
//
|
||||
//go:inline
|
||||
func Size[A any](as NonEmptyArray[A]) int {
|
||||
return G.Size(as)
|
||||
}
|
||||
|
||||
// Flatten flattens a NonEmptyArray of NonEmptyArrays into a single NonEmptyArray.
|
||||
// This operation concatenates all inner arrays into one.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type
|
||||
//
|
||||
// Parameters:
|
||||
// - mma: A NonEmptyArray of NonEmptyArrays
|
||||
//
|
||||
// Returns:
|
||||
// - NonEmptyArray[A]: A flattened NonEmptyArray containing all elements
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// nested := From(From(1, 2), From(3, 4), From(5))
|
||||
// flat := Flatten(nested) // NonEmptyArray[int]{1, 2, 3, 4, 5}
|
||||
func Flatten[A any](mma NonEmptyArray[NonEmptyArray[A]]) NonEmptyArray[A] {
|
||||
return G.Flatten(mma)
|
||||
}
|
||||
|
||||
// MonadChain applies a function that returns a NonEmptyArray to each element and flattens the results.
|
||||
// This is the monadic bind operation (flatMap) that takes the array as the first parameter.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: The input NonEmptyArray
|
||||
// - f: A function that takes an element and returns a NonEmptyArray
|
||||
//
|
||||
// Returns:
|
||||
// - NonEmptyArray[B]: The flattened result
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// arr := From(1, 2, 3)
|
||||
// result := MonadChain(arr, func(x int) NonEmptyArray[int] {
|
||||
// return From(x, x*10)
|
||||
// }) // NonEmptyArray[int]{1, 10, 2, 20, 3, 30}
|
||||
func MonadChain[A, B any](fa NonEmptyArray[A], f Kleisli[A, B]) NonEmptyArray[B] {
|
||||
return G.MonadChain(fa, f)
|
||||
}
|
||||
|
||||
// Chain applies a function that returns a NonEmptyArray to each element and flattens the results.
|
||||
// This is the curried version of MonadChain.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that takes an element and returns a NonEmptyArray
|
||||
//
|
||||
// Returns:
|
||||
// - Operator[A, B]: A function that transforms NonEmptyArray[A] to NonEmptyArray[B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// duplicate := Chain(func(x int) NonEmptyArray[int] { return From(x, x) })
|
||||
// result := duplicate(From(1, 2, 3)) // NonEmptyArray[int]{1, 1, 2, 2, 3, 3}
|
||||
func Chain[A, B any](f func(A) NonEmptyArray[B]) Operator[A, B] {
|
||||
return G.Chain[NonEmptyArray[A]](f)
|
||||
}
|
||||
|
||||
// MonadAp applies a NonEmptyArray of functions to a NonEmptyArray of values.
|
||||
// Each function is applied to each value, producing a cartesian product of results.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - B: The output element type
|
||||
// - A: The input element type
|
||||
//
|
||||
// Parameters:
|
||||
// - fab: A NonEmptyArray of functions
|
||||
// - fa: A NonEmptyArray of values
|
||||
//
|
||||
// Returns:
|
||||
// - NonEmptyArray[B]: The result of applying all functions to all values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// fns := From(func(x int) int { return x * 2 }, func(x int) int { return x + 10 })
|
||||
// vals := From(1, 2)
|
||||
// result := MonadAp(fns, vals) // NonEmptyArray[int]{2, 4, 11, 12}
|
||||
func MonadAp[B, A any](fab NonEmptyArray[func(A) B], fa NonEmptyArray[A]) NonEmptyArray[B] {
|
||||
return G.MonadAp[NonEmptyArray[B]](fab, fa)
|
||||
}
|
||||
|
||||
// Ap applies a NonEmptyArray of functions to a NonEmptyArray of values.
|
||||
// This is the curried version of MonadAp.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - B: The output element type
|
||||
// - A: The input element type
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: A NonEmptyArray of values
|
||||
//
|
||||
// Returns:
|
||||
// - func(NonEmptyArray[func(A) B]) NonEmptyArray[B]: A function that applies functions to the values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// vals := From(1, 2)
|
||||
// applyTo := Ap[int](vals)
|
||||
// fns := From(func(x int) int { return x * 2 }, func(x int) int { return x + 10 })
|
||||
// result := applyTo(fns) // NonEmptyArray[int]{2, 4, 11, 12}
|
||||
func Ap[B, A any](fa NonEmptyArray[A]) func(NonEmptyArray[func(A) B]) NonEmptyArray[B] {
|
||||
return G.Ap[NonEmptyArray[B], NonEmptyArray[func(A) B]](fa)
|
||||
}
|
||||
@@ -136,7 +448,23 @@ func Fold[A any](s S.Semigroup[A]) func(NonEmptyArray[A]) A {
|
||||
}
|
||||
}
|
||||
|
||||
// Prepend prepends a single value to an array
|
||||
// Prepend prepends a single value to the beginning of a NonEmptyArray.
|
||||
// Returns a new NonEmptyArray with the value at the front.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type
|
||||
//
|
||||
// Parameters:
|
||||
// - head: The value to prepend
|
||||
//
|
||||
// Returns:
|
||||
// - EM.Endomorphism[NonEmptyArray[A]]: A function that prepends the value to a NonEmptyArray
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// arr := From(2, 3, 4)
|
||||
// prepend1 := Prepend(1)
|
||||
// result := prepend1(arr) // NonEmptyArray[int]{1, 2, 3, 4}
|
||||
func Prepend[A any](head A) EM.Endomorphism[NonEmptyArray[A]] {
|
||||
return array.Prepend[EM.Endomorphism[NonEmptyArray[A]]](head)
|
||||
}
|
||||
@@ -226,3 +554,59 @@ func ToNonEmptyArray[A any](as []A) Option[NonEmptyArray[A]] {
|
||||
}
|
||||
return option.Some(NonEmptyArray[A](as))
|
||||
}
|
||||
|
||||
// Extract returns the first element of a NonEmptyArray.
|
||||
// This is an alias for Head and is part of the Comonad interface.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The input NonEmptyArray
|
||||
//
|
||||
// Returns:
|
||||
// - A: The first element
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// arr := From(1, 2, 3)
|
||||
// first := Extract(arr) // 1
|
||||
//
|
||||
//go:inline
|
||||
func Extract[A any](as NonEmptyArray[A]) A {
|
||||
return Head(as)
|
||||
}
|
||||
|
||||
// Extend applies a function to all suffixes of a NonEmptyArray.
|
||||
// For each position i, it applies the function to the subarray starting at position i.
|
||||
// This is part of the Comonad interface.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that takes a NonEmptyArray and returns a value
|
||||
//
|
||||
// Returns:
|
||||
// - Operator[A, B]: A function that transforms NonEmptyArray[A] to NonEmptyArray[B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// arr := From(1, 2, 3, 4)
|
||||
// sumSuffix := Extend(func(xs NonEmptyArray[int]) int {
|
||||
// sum := 0
|
||||
// for _, x := range xs {
|
||||
// sum += x
|
||||
// }
|
||||
// return sum
|
||||
// })
|
||||
// result := sumSuffix(arr) // NonEmptyArray[int]{10, 9, 7, 4}
|
||||
// // [1,2,3,4] -> 10, [2,3,4] -> 9, [3,4] -> 7, [4] -> 4
|
||||
//
|
||||
//go:inline
|
||||
func Extend[A, B any](f func(NonEmptyArray[A]) B) Operator[A, B] {
|
||||
return func(as NonEmptyArray[A]) NonEmptyArray[B] {
|
||||
return G.MakeBy[NonEmptyArray[B]](len(as), func(i int) B { return f(as[i:]) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,13 @@
|
||||
package nonempty
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
STR "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -368,3 +371,522 @@ func TestToNonEmptyArrayUseCases(t *testing.T) {
|
||||
assert.Equal(t, "default", result2)
|
||||
})
|
||||
}
|
||||
|
||||
// TestOf tests the Of function
|
||||
func TestOf(t *testing.T) {
|
||||
t.Run("Create single element array with int", func(t *testing.T) {
|
||||
arr := Of(42)
|
||||
assert.Equal(t, 1, Size(arr))
|
||||
assert.Equal(t, 42, Head(arr))
|
||||
})
|
||||
|
||||
t.Run("Create single element array with string", func(t *testing.T) {
|
||||
arr := Of("hello")
|
||||
assert.Equal(t, 1, Size(arr))
|
||||
assert.Equal(t, "hello", Head(arr))
|
||||
})
|
||||
|
||||
t.Run("Create single element array with struct", func(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
person := Person{Name: "Alice", Age: 30}
|
||||
arr := Of(person)
|
||||
assert.Equal(t, 1, Size(arr))
|
||||
assert.Equal(t, "Alice", Head(arr).Name)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFrom tests the From function
|
||||
func TestFrom(t *testing.T) {
|
||||
t.Run("Create array with single element", func(t *testing.T) {
|
||||
arr := From(1)
|
||||
assert.Equal(t, 1, Size(arr))
|
||||
assert.Equal(t, 1, Head(arr))
|
||||
})
|
||||
|
||||
t.Run("Create array with multiple elements", func(t *testing.T) {
|
||||
arr := From(1, 2, 3, 4, 5)
|
||||
assert.Equal(t, 5, Size(arr))
|
||||
assert.Equal(t, 1, Head(arr))
|
||||
assert.Equal(t, 5, Last(arr))
|
||||
})
|
||||
|
||||
t.Run("Create array with strings", func(t *testing.T) {
|
||||
arr := From("a", "b", "c")
|
||||
assert.Equal(t, 3, Size(arr))
|
||||
assert.Equal(t, "a", Head(arr))
|
||||
assert.Equal(t, "c", Last(arr))
|
||||
})
|
||||
}
|
||||
|
||||
// TestIsEmpty tests the IsEmpty function
|
||||
func TestIsEmpty(t *testing.T) {
|
||||
t.Run("IsEmpty always returns false", func(t *testing.T) {
|
||||
arr := From(1, 2, 3)
|
||||
assert.False(t, IsEmpty(arr))
|
||||
})
|
||||
|
||||
t.Run("IsEmpty returns false for single element", func(t *testing.T) {
|
||||
arr := Of(1)
|
||||
assert.False(t, IsEmpty(arr))
|
||||
})
|
||||
}
|
||||
|
||||
// TestIsNonEmpty tests the IsNonEmpty function
|
||||
func TestIsNonEmpty(t *testing.T) {
|
||||
t.Run("IsNonEmpty always returns true", func(t *testing.T) {
|
||||
arr := From(1, 2, 3)
|
||||
assert.True(t, IsNonEmpty(arr))
|
||||
})
|
||||
|
||||
t.Run("IsNonEmpty returns true for single element", func(t *testing.T) {
|
||||
arr := Of(1)
|
||||
assert.True(t, IsNonEmpty(arr))
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadMap tests the MonadMap function
|
||||
func TestMonadMap(t *testing.T) {
|
||||
t.Run("Map integers to doubles", func(t *testing.T) {
|
||||
arr := From(1, 2, 3, 4)
|
||||
result := MonadMap(arr, func(x int) int { return x * 2 })
|
||||
assert.Equal(t, 4, Size(result))
|
||||
assert.Equal(t, 2, Head(result))
|
||||
assert.Equal(t, 8, Last(result))
|
||||
})
|
||||
|
||||
t.Run("Map strings to lengths", func(t *testing.T) {
|
||||
arr := From("a", "bb", "ccc")
|
||||
result := MonadMap(arr, func(s string) int { return len(s) })
|
||||
assert.Equal(t, 3, Size(result))
|
||||
assert.Equal(t, 1, Head(result))
|
||||
assert.Equal(t, 3, Last(result))
|
||||
})
|
||||
|
||||
t.Run("Map single element", func(t *testing.T) {
|
||||
arr := Of(5)
|
||||
result := MonadMap(arr, func(x int) int { return x * 10 })
|
||||
assert.Equal(t, 1, Size(result))
|
||||
assert.Equal(t, 50, Head(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestMap tests the Map function
|
||||
func TestMap(t *testing.T) {
|
||||
t.Run("Curried map with integers", func(t *testing.T) {
|
||||
double := Map(func(x int) int { return x * 2 })
|
||||
arr := From(1, 2, 3)
|
||||
result := double(arr)
|
||||
assert.Equal(t, 3, Size(result))
|
||||
assert.Equal(t, 2, Head(result))
|
||||
assert.Equal(t, 6, Last(result))
|
||||
})
|
||||
|
||||
t.Run("Curried map with strings", func(t *testing.T) {
|
||||
toUpper := Map(func(s string) string { return s + "!" })
|
||||
arr := From("hello", "world")
|
||||
result := toUpper(arr)
|
||||
assert.Equal(t, 2, Size(result))
|
||||
assert.Equal(t, "hello!", Head(result))
|
||||
assert.Equal(t, "world!", Last(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestReduce tests the Reduce function
|
||||
func TestReduce(t *testing.T) {
|
||||
t.Run("Sum integers", func(t *testing.T) {
|
||||
sum := Reduce(func(acc int, x int) int { return acc + x }, 0)
|
||||
arr := From(1, 2, 3, 4, 5)
|
||||
result := sum(arr)
|
||||
assert.Equal(t, 15, result)
|
||||
})
|
||||
|
||||
t.Run("Concatenate strings", func(t *testing.T) {
|
||||
concat := Reduce(func(acc string, x string) string { return acc + x }, "")
|
||||
arr := From("a", "b", "c")
|
||||
result := concat(arr)
|
||||
assert.Equal(t, "abc", result)
|
||||
})
|
||||
|
||||
t.Run("Product of numbers", func(t *testing.T) {
|
||||
product := Reduce(func(acc int, x int) int { return acc * x }, 1)
|
||||
arr := From(2, 3, 4)
|
||||
result := product(arr)
|
||||
assert.Equal(t, 24, result)
|
||||
})
|
||||
|
||||
t.Run("Reduce single element", func(t *testing.T) {
|
||||
sum := Reduce(func(acc int, x int) int { return acc + x }, 10)
|
||||
arr := Of(5)
|
||||
result := sum(arr)
|
||||
assert.Equal(t, 15, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestReduceRight tests the ReduceRight function
|
||||
func TestReduceRight(t *testing.T) {
|
||||
t.Run("Concatenate strings right to left", func(t *testing.T) {
|
||||
concat := ReduceRight(func(x string, acc string) string { return acc + x }, "")
|
||||
arr := From("a", "b", "c")
|
||||
result := concat(arr)
|
||||
assert.Equal(t, "cba", result)
|
||||
})
|
||||
|
||||
t.Run("Build list right to left", func(t *testing.T) {
|
||||
buildList := ReduceRight(func(x int, acc []int) []int { return append(acc, x) }, []int{})
|
||||
arr := From(1, 2, 3)
|
||||
result := buildList(arr)
|
||||
assert.Equal(t, []int{3, 2, 1}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTail tests the Tail function
|
||||
func TestTail(t *testing.T) {
|
||||
t.Run("Get tail of multi-element array", func(t *testing.T) {
|
||||
arr := From(1, 2, 3, 4)
|
||||
tail := Tail(arr)
|
||||
assert.Equal(t, 3, len(tail))
|
||||
assert.Equal(t, []int{2, 3, 4}, tail)
|
||||
})
|
||||
|
||||
t.Run("Get tail of single element array", func(t *testing.T) {
|
||||
arr := Of(1)
|
||||
tail := Tail(arr)
|
||||
assert.Equal(t, 0, len(tail))
|
||||
assert.Equal(t, []int{}, tail)
|
||||
})
|
||||
|
||||
t.Run("Get tail of two element array", func(t *testing.T) {
|
||||
arr := From(1, 2)
|
||||
tail := Tail(arr)
|
||||
assert.Equal(t, 1, len(tail))
|
||||
assert.Equal(t, []int{2}, tail)
|
||||
})
|
||||
}
|
||||
|
||||
// TestHead tests the Head function
|
||||
func TestHead(t *testing.T) {
|
||||
t.Run("Get head of multi-element array", func(t *testing.T) {
|
||||
arr := From(1, 2, 3)
|
||||
head := Head(arr)
|
||||
assert.Equal(t, 1, head)
|
||||
})
|
||||
|
||||
t.Run("Get head of single element array", func(t *testing.T) {
|
||||
arr := Of(42)
|
||||
head := Head(arr)
|
||||
assert.Equal(t, 42, head)
|
||||
})
|
||||
|
||||
t.Run("Get head of string array", func(t *testing.T) {
|
||||
arr := From("first", "second", "third")
|
||||
head := Head(arr)
|
||||
assert.Equal(t, "first", head)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirst tests the First function
|
||||
func TestFirst(t *testing.T) {
|
||||
t.Run("First is alias for Head", func(t *testing.T) {
|
||||
arr := From(1, 2, 3)
|
||||
assert.Equal(t, Head(arr), First(arr))
|
||||
})
|
||||
|
||||
t.Run("Get first element", func(t *testing.T) {
|
||||
arr := From("a", "b", "c")
|
||||
first := First(arr)
|
||||
assert.Equal(t, "a", first)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLast tests the Last function
|
||||
func TestLast(t *testing.T) {
|
||||
t.Run("Get last of multi-element array", func(t *testing.T) {
|
||||
arr := From(1, 2, 3, 4, 5)
|
||||
last := Last(arr)
|
||||
assert.Equal(t, 5, last)
|
||||
})
|
||||
|
||||
t.Run("Get last of single element array", func(t *testing.T) {
|
||||
arr := Of(42)
|
||||
last := Last(arr)
|
||||
assert.Equal(t, 42, last)
|
||||
})
|
||||
|
||||
t.Run("Get last of string array", func(t *testing.T) {
|
||||
arr := From("first", "second", "third")
|
||||
last := Last(arr)
|
||||
assert.Equal(t, "third", last)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSize tests the Size function
|
||||
func TestSize(t *testing.T) {
|
||||
t.Run("Size of multi-element array", func(t *testing.T) {
|
||||
arr := From(1, 2, 3, 4, 5)
|
||||
size := Size(arr)
|
||||
assert.Equal(t, 5, size)
|
||||
})
|
||||
|
||||
t.Run("Size of single element array", func(t *testing.T) {
|
||||
arr := Of(1)
|
||||
size := Size(arr)
|
||||
assert.Equal(t, 1, size)
|
||||
})
|
||||
|
||||
t.Run("Size of large array", func(t *testing.T) {
|
||||
elements := make([]int, 1000)
|
||||
arr := From(1, elements...)
|
||||
size := Size(arr)
|
||||
assert.Equal(t, 1001, size)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFlatten tests the Flatten function
|
||||
func TestFlatten(t *testing.T) {
|
||||
t.Run("Flatten nested arrays", func(t *testing.T) {
|
||||
nested := From(From(1, 2), From(3, 4), From(5))
|
||||
flat := Flatten(nested)
|
||||
assert.Equal(t, 5, Size(flat))
|
||||
assert.Equal(t, 1, Head(flat))
|
||||
assert.Equal(t, 5, Last(flat))
|
||||
})
|
||||
|
||||
t.Run("Flatten single nested array", func(t *testing.T) {
|
||||
nested := Of(From(1, 2, 3))
|
||||
flat := Flatten(nested)
|
||||
assert.Equal(t, 3, Size(flat))
|
||||
assert.Equal(t, []int{1, 2, 3}, []int(flat))
|
||||
})
|
||||
|
||||
t.Run("Flatten arrays of different sizes", func(t *testing.T) {
|
||||
nested := From(Of(1), From(2, 3, 4), From(5, 6))
|
||||
flat := Flatten(nested)
|
||||
assert.Equal(t, 6, Size(flat))
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5, 6}, []int(flat))
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadChain tests the MonadChain function
|
||||
func TestMonadChain(t *testing.T) {
|
||||
t.Run("Chain with duplication", func(t *testing.T) {
|
||||
arr := From(1, 2, 3)
|
||||
result := MonadChain(arr, func(x int) NonEmptyArray[int] {
|
||||
return From(x, x*10)
|
||||
})
|
||||
assert.Equal(t, 6, Size(result))
|
||||
assert.Equal(t, []int{1, 10, 2, 20, 3, 30}, []int(result))
|
||||
})
|
||||
|
||||
t.Run("Chain with expansion", func(t *testing.T) {
|
||||
arr := From(1, 2)
|
||||
result := MonadChain(arr, func(x int) NonEmptyArray[int] {
|
||||
return From(x, x+1, x+2)
|
||||
})
|
||||
assert.Equal(t, 6, Size(result))
|
||||
assert.Equal(t, []int{1, 2, 3, 2, 3, 4}, []int(result))
|
||||
})
|
||||
|
||||
t.Run("Chain single element", func(t *testing.T) {
|
||||
arr := Of(5)
|
||||
result := MonadChain(arr, func(x int) NonEmptyArray[int] {
|
||||
return From(x, x*2)
|
||||
})
|
||||
assert.Equal(t, 2, Size(result))
|
||||
assert.Equal(t, []int{5, 10}, []int(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestChain tests the Chain function
|
||||
func TestChain(t *testing.T) {
|
||||
t.Run("Curried chain with duplication", func(t *testing.T) {
|
||||
duplicate := Chain(func(x int) NonEmptyArray[int] {
|
||||
return From(x, x)
|
||||
})
|
||||
arr := From(1, 2, 3)
|
||||
result := duplicate(arr)
|
||||
assert.Equal(t, 6, Size(result))
|
||||
assert.Equal(t, []int{1, 1, 2, 2, 3, 3}, []int(result))
|
||||
})
|
||||
|
||||
t.Run("Curried chain with transformation", func(t *testing.T) {
|
||||
expand := Chain(func(x int) NonEmptyArray[string] {
|
||||
return Of(fmt.Sprintf("%d", x))
|
||||
})
|
||||
arr := From(1, 2, 3)
|
||||
result := expand(arr)
|
||||
assert.Equal(t, 3, Size(result))
|
||||
assert.Equal(t, "1", Head(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadAp tests the MonadAp function
|
||||
func TestMonadAp(t *testing.T) {
|
||||
t.Run("Apply functions to values", func(t *testing.T) {
|
||||
fns := From(
|
||||
func(x int) int { return x * 2 },
|
||||
func(x int) int { return x + 10 },
|
||||
)
|
||||
vals := From(1, 2)
|
||||
result := MonadAp(fns, vals)
|
||||
assert.Equal(t, 4, Size(result))
|
||||
assert.Equal(t, []int{2, 4, 11, 12}, []int(result))
|
||||
})
|
||||
|
||||
t.Run("Apply single function to multiple values", func(t *testing.T) {
|
||||
fns := Of(func(x int) int { return x * 3 })
|
||||
vals := From(1, 2, 3)
|
||||
result := MonadAp(fns, vals)
|
||||
assert.Equal(t, 3, Size(result))
|
||||
assert.Equal(t, []int{3, 6, 9}, []int(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestAp tests the Ap function
|
||||
func TestAp(t *testing.T) {
|
||||
t.Run("Curried apply", func(t *testing.T) {
|
||||
vals := From(1, 2)
|
||||
applyTo := Ap[int](vals)
|
||||
fns := From(
|
||||
func(x int) int { return x * 2 },
|
||||
func(x int) int { return x + 10 },
|
||||
)
|
||||
result := applyTo(fns)
|
||||
assert.Equal(t, 4, Size(result))
|
||||
assert.Equal(t, []int{2, 4, 11, 12}, []int(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestFoldMap tests the FoldMap function
|
||||
func TestFoldMap(t *testing.T) {
|
||||
t.Run("FoldMap with sum semigroup", func(t *testing.T) {
|
||||
sumSemigroup := N.SemigroupSum[int]()
|
||||
arr := From(1, 2, 3, 4)
|
||||
result := FoldMap[int, int](sumSemigroup)(func(x int) int { return x * 2 })(arr)
|
||||
assert.Equal(t, 20, result) // (1*2) + (2*2) + (3*2) + (4*2) = 20
|
||||
})
|
||||
|
||||
t.Run("FoldMap with string concatenation", func(t *testing.T) {
|
||||
concatSemigroup := STR.Semigroup
|
||||
arr := From(1, 2, 3)
|
||||
result := FoldMap[int, string](concatSemigroup)(func(x int) string { return fmt.Sprintf("%d", x) })(arr)
|
||||
assert.Equal(t, "123", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFold tests the Fold function
|
||||
func TestFold(t *testing.T) {
|
||||
t.Run("Fold with sum semigroup", func(t *testing.T) {
|
||||
sumSemigroup := N.SemigroupSum[int]()
|
||||
arr := From(1, 2, 3, 4, 5)
|
||||
result := Fold(sumSemigroup)(arr)
|
||||
assert.Equal(t, 15, result)
|
||||
})
|
||||
|
||||
t.Run("Fold with string concatenation", func(t *testing.T) {
|
||||
concatSemigroup := STR.Semigroup
|
||||
arr := From("a", "b", "c")
|
||||
result := Fold(concatSemigroup)(arr)
|
||||
assert.Equal(t, "abc", result)
|
||||
})
|
||||
|
||||
t.Run("Fold single element", func(t *testing.T) {
|
||||
sumSemigroup := N.SemigroupSum[int]()
|
||||
arr := Of(42)
|
||||
result := Fold(sumSemigroup)(arr)
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPrepend tests the Prepend function
|
||||
func TestPrepend(t *testing.T) {
|
||||
t.Run("Prepend to multi-element array", func(t *testing.T) {
|
||||
arr := From(2, 3, 4)
|
||||
prepend1 := Prepend(1)
|
||||
result := prepend1(arr)
|
||||
assert.Equal(t, 4, Size(result))
|
||||
assert.Equal(t, 1, Head(result))
|
||||
assert.Equal(t, 4, Last(result))
|
||||
})
|
||||
|
||||
t.Run("Prepend to single element array", func(t *testing.T) {
|
||||
arr := Of(2)
|
||||
prepend1 := Prepend(1)
|
||||
result := prepend1(arr)
|
||||
assert.Equal(t, 2, Size(result))
|
||||
assert.Equal(t, []int{1, 2}, []int(result))
|
||||
})
|
||||
|
||||
t.Run("Prepend string", func(t *testing.T) {
|
||||
arr := From("world")
|
||||
prependHello := Prepend("hello")
|
||||
result := prependHello(arr)
|
||||
assert.Equal(t, 2, Size(result))
|
||||
assert.Equal(t, "hello", Head(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestExtract tests the Extract function
|
||||
func TestExtract(t *testing.T) {
|
||||
t.Run("Extract from multi-element array", func(t *testing.T) {
|
||||
arr := From(1, 2, 3)
|
||||
result := Extract(arr)
|
||||
assert.Equal(t, 1, result)
|
||||
})
|
||||
|
||||
t.Run("Extract from single element array", func(t *testing.T) {
|
||||
arr := Of(42)
|
||||
result := Extract(arr)
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
|
||||
t.Run("Extract is same as Head", func(t *testing.T) {
|
||||
arr := From("a", "b", "c")
|
||||
assert.Equal(t, Head(arr), Extract(arr))
|
||||
})
|
||||
}
|
||||
|
||||
// TestExtend tests the Extend function
|
||||
func TestExtend(t *testing.T) {
|
||||
t.Run("Extend with sum of suffixes", func(t *testing.T) {
|
||||
arr := From(1, 2, 3, 4)
|
||||
sumSuffix := Extend(func(xs NonEmptyArray[int]) int {
|
||||
sum := 0
|
||||
for _, x := range xs {
|
||||
sum += x
|
||||
}
|
||||
return sum
|
||||
})
|
||||
result := sumSuffix(arr)
|
||||
assert.Equal(t, 4, Size(result))
|
||||
assert.Equal(t, []int{10, 9, 7, 4}, []int(result))
|
||||
})
|
||||
|
||||
t.Run("Extend with head of suffixes", func(t *testing.T) {
|
||||
arr := From(1, 2, 3)
|
||||
getHeads := Extend(Head[int])
|
||||
result := getHeads(arr)
|
||||
assert.Equal(t, 3, Size(result))
|
||||
assert.Equal(t, []int{1, 2, 3}, []int(result))
|
||||
})
|
||||
|
||||
t.Run("Extend with size of suffixes", func(t *testing.T) {
|
||||
arr := From("a", "b", "c", "d")
|
||||
getSizes := Extend(Size[string])
|
||||
result := getSizes(arr)
|
||||
assert.Equal(t, 4, Size(result))
|
||||
assert.Equal(t, []int{4, 3, 2, 1}, []int(result))
|
||||
})
|
||||
|
||||
t.Run("Extend single element", func(t *testing.T) {
|
||||
arr := Of(5)
|
||||
double := Extend(func(xs NonEmptyArray[int]) int {
|
||||
return Head(xs) * 2
|
||||
})
|
||||
result := double(arr)
|
||||
assert.Equal(t, 1, Size(result))
|
||||
assert.Equal(t, 10, Head(result))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -519,6 +519,8 @@ func RunAll(testcases map[string]Reader) Reader {
|
||||
// by providing a function that converts R2 to R1. This allows you to focus a test on a
|
||||
// specific property or subset of a larger data structure.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// This is particularly useful when you have an assertion that operates on a specific field
|
||||
// or property, and you want to apply it to a complete object. Instead of extracting the
|
||||
// property and then asserting on it, you can transform the assertion to work directly
|
||||
|
||||
@@ -19,11 +19,13 @@ package consumer
|
||||
// This is the contravariant map operation for Consumers, analogous to reader.Local
|
||||
// but operating on the input side rather than the output side.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// Given a Consumer[R1] that consumes values of type R1, and a function f that
|
||||
// converts R2 to R1, Local creates a new Consumer[R2] that:
|
||||
// 1. Takes a value of type R2
|
||||
// 2. Applies f to convert it to R1
|
||||
// 3. Passes the result to the original Consumer[R1]
|
||||
// 1. Takes a value of type R2
|
||||
// 2. Applies f to convert it to R1
|
||||
// 3. Passes the result to the original Consumer[R1]
|
||||
//
|
||||
// This is particularly useful for adapting consumers to work with different input types,
|
||||
// similar to how reader.Local adapts readers to work with different environment types.
|
||||
@@ -168,7 +170,7 @@ package consumer
|
||||
// - reader.Local transforms the environment before reading
|
||||
// - consumer.Local transforms the input before consuming
|
||||
// - Both are contravariant functors on their input type
|
||||
func Local[R2, R1 any](f func(R2) R1) Operator[R1, R2] {
|
||||
func Local[R1, R2 any](f func(R2) R1) Operator[R1, R2] {
|
||||
return func(c Consumer[R1]) Consumer[R2] {
|
||||
return func(r2 R2) {
|
||||
c(f(r2))
|
||||
|
||||
74
v2/context/readerio/profunctor.go
Normal file
74
v2/context/readerio/profunctor.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerio
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a context-based ReaderIO.
|
||||
// It applies f to the input context (contravariantly) and g to the output value (covariantly).
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// This operation allows you to:
|
||||
// - Modify the context before passing it to the ReaderIO (via f)
|
||||
// - Transform the result value after the IO effect completes (via g)
|
||||
//
|
||||
// The function f returns both a new context and a CancelFunc that should be called to release resources.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The original result type produced by the ReaderIO
|
||||
// - B: The new output result type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the input context (contravariant)
|
||||
// - g: Function to transform the output value from A to B (covariant)
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a ReaderIO[A] and returns a ReaderIO[B]
|
||||
//
|
||||
//go:inline
|
||||
func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFunc), g func(A) B) Operator[A, B] {
|
||||
return function.Flow2(
|
||||
Local[A](f),
|
||||
Map(g),
|
||||
)
|
||||
}
|
||||
|
||||
// Contramap changes the context during the execution of a ReaderIO.
|
||||
// This is the contravariant functor operation that transforms the input context.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// Contramap is an alias for Local and is useful for adapting a ReaderIO to work with
|
||||
// a modified context by providing a function that transforms the context.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The result type (unchanged)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the context, returning a new context and CancelFunc
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a ReaderIO[A] and returns a ReaderIO[A]
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
|
||||
return Local[A](f)
|
||||
}
|
||||
97
v2/context/readerio/profunctor_test.go
Normal file
97
v2/context/readerio/profunctor_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestPromapBasic tests basic Promap functionality
|
||||
func TestPromapBasic(t *testing.T) {
|
||||
t.Run("transform both context and output", func(t *testing.T) {
|
||||
// ReaderIO that reads a value from context
|
||||
getValue := func(ctx context.Context) IO[int] {
|
||||
return func() int {
|
||||
if v := ctx.Value("key"); v != nil {
|
||||
return v.(int)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// Transform context and result
|
||||
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
newCtx := context.WithValue(ctx, "key", 42)
|
||||
return newCtx, func() {}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap(addKey, toString)(getValue)
|
||||
result := adapted(context.Background())()
|
||||
|
||||
assert.Equal(t, "42", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestContramapBasic tests basic Contramap functionality
|
||||
func TestContramapBasic(t *testing.T) {
|
||||
t.Run("context transformation", func(t *testing.T) {
|
||||
getValue := func(ctx context.Context) IO[int] {
|
||||
return func() int {
|
||||
if v := ctx.Value("key"); v != nil {
|
||||
return v.(int)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
newCtx := context.WithValue(ctx, "key", 100)
|
||||
return newCtx, func() {}
|
||||
}
|
||||
|
||||
adapted := Contramap[int](addKey)(getValue)
|
||||
result := adapted(context.Background())()
|
||||
|
||||
assert.Equal(t, 100, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalBasic tests basic Local functionality
|
||||
func TestLocalBasic(t *testing.T) {
|
||||
t.Run("adds timeout to context", func(t *testing.T) {
|
||||
getValue := func(ctx context.Context) IO[bool] {
|
||||
return func() bool {
|
||||
_, hasDeadline := ctx.Deadline()
|
||||
return hasDeadline
|
||||
}
|
||||
}
|
||||
|
||||
addTimeout := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(ctx, time.Second)
|
||||
}
|
||||
|
||||
adapted := Local[bool](addTimeout)(getValue)
|
||||
result := adapted(context.Background())()
|
||||
|
||||
assert.True(t, result)
|
||||
})
|
||||
}
|
||||
@@ -608,7 +608,7 @@ func TestCircuitBreaker_ErrorMessageFormat(t *testing.T) {
|
||||
protectedOp := pair.Tail(resultEnv)
|
||||
outcome := protectedOp(ctx)()
|
||||
|
||||
assert.True(t, result.IsLeft[string](outcome))
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
|
||||
// Error message should indicate circuit breaker is open
|
||||
_, err := result.Unwrap(outcome)
|
||||
|
||||
75
v2/context/readerioresult/profunctor.go
Normal file
75
v2/context/readerioresult/profunctor.go
Normal file
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a context-based ReaderIOResult.
|
||||
// It applies f to the input context (contravariantly) and g to the output value (covariantly).
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// This operation allows you to:
|
||||
// - Modify the context before passing it to the ReaderIOResult (via f)
|
||||
// - Transform the success value after the IO effect completes (via g)
|
||||
//
|
||||
// The function f returns both a new context and a CancelFunc that should be called to release resources.
|
||||
// The error type is fixed as error and remains unchanged through the transformation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The original success type produced by the ReaderIOResult
|
||||
// - B: The new output success type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the input context (contravariant)
|
||||
// - g: Function to transform the output success value from A to B (covariant)
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a ReaderIOResult[A] and returns a ReaderIOResult[B]
|
||||
//
|
||||
//go:inline
|
||||
func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFunc), g func(A) B) Operator[A, B] {
|
||||
return function.Flow2(
|
||||
Local[A](f),
|
||||
Map(g),
|
||||
)
|
||||
}
|
||||
|
||||
// Contramap changes the context during the execution of a ReaderIOResult.
|
||||
// This is the contravariant functor operation that transforms the input context.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// Contramap is an alias for Local and is useful for adapting a ReaderIOResult to work with
|
||||
// a modified context by providing a function that transforms the context.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type (unchanged)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the context, returning a new context and CancelFunc
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a ReaderIOResult[A] and returns a ReaderIOResult[A]
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
|
||||
return Local[A](f)
|
||||
}
|
||||
98
v2/context/readerioresult/profunctor_test.go
Normal file
98
v2/context/readerioresult/profunctor_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
R "github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestPromapBasic tests basic Promap functionality
|
||||
func TestPromapBasic(t *testing.T) {
|
||||
t.Run("transform both context and output", func(t *testing.T) {
|
||||
getValue := func(ctx context.Context) IOResult[int] {
|
||||
return func() R.Result[int] {
|
||||
if v := ctx.Value("key"); v != nil {
|
||||
return R.Of(v.(int))
|
||||
}
|
||||
return R.Of(0)
|
||||
}
|
||||
}
|
||||
|
||||
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
newCtx := context.WithValue(ctx, "key", 42)
|
||||
return newCtx, func() {}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap(addKey, toString)(getValue)
|
||||
result := adapted(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of("42"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestContramapBasic tests basic Contramap functionality
|
||||
func TestContramapBasic(t *testing.T) {
|
||||
t.Run("context transformation", func(t *testing.T) {
|
||||
getValue := func(ctx context.Context) IOResult[int] {
|
||||
return func() R.Result[int] {
|
||||
if v := ctx.Value("key"); v != nil {
|
||||
return R.Of(v.(int))
|
||||
}
|
||||
return R.Of(0)
|
||||
}
|
||||
}
|
||||
|
||||
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
newCtx := context.WithValue(ctx, "key", 100)
|
||||
return newCtx, func() {}
|
||||
}
|
||||
|
||||
adapted := Contramap[int](addKey)(getValue)
|
||||
result := adapted(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of(100), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalBasic tests basic Local functionality
|
||||
func TestLocalBasic(t *testing.T) {
|
||||
t.Run("adds value to context", func(t *testing.T) {
|
||||
getValue := func(ctx context.Context) IOResult[string] {
|
||||
return func() R.Result[string] {
|
||||
if v := ctx.Value("user"); v != nil {
|
||||
return R.Of(v.(string))
|
||||
}
|
||||
return R.Of("unknown")
|
||||
}
|
||||
}
|
||||
|
||||
addUser := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
newCtx := context.WithValue(ctx, "user", "Alice")
|
||||
return newCtx, func() {}
|
||||
}
|
||||
|
||||
adapted := Local[string](addUser)(getValue)
|
||||
result := adapted(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of("Alice"), result)
|
||||
})
|
||||
}
|
||||
106
v2/context/readerresult/profunctor.go
Normal file
106
v2/context/readerresult/profunctor.go
Normal file
@@ -0,0 +1,106 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a context-based ReaderResult.
|
||||
// It applies f to the input context (contravariantly) and g to the output value (covariantly).
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// This operation allows you to:
|
||||
// - Modify the context before passing it to the ReaderResult (via f)
|
||||
// - Transform the success value after the computation completes (via g)
|
||||
//
|
||||
// The function f returns both a new context and a CancelFunc that should be called to release resources.
|
||||
// The error type is fixed as error and remains unchanged through the transformation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The original success type produced by the ReaderResult
|
||||
// - B: The new output success type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the input context (contravariant)
|
||||
// - g: Function to transform the output success value from A to B (covariant)
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a ReaderResult[A] and returns a ReaderResult[B]
|
||||
//
|
||||
//go:inline
|
||||
func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFunc), g func(A) B) Operator[A, B] {
|
||||
return function.Flow2(
|
||||
Local[A](f),
|
||||
Map(g),
|
||||
)
|
||||
}
|
||||
|
||||
// Contramap changes the context during the execution of a ReaderResult.
|
||||
// This is the contravariant functor operation that transforms the input context.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// Contramap is an alias for Local and is useful for adapting a ReaderResult to work with
|
||||
// a modified context by providing a function that transforms the context.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type (unchanged)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the context, returning a new context and CancelFunc
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a ReaderResult[A] and returns a ReaderResult[A]
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
|
||||
return Local[A](f)
|
||||
}
|
||||
|
||||
// Local changes the context during the execution of a ReaderResult.
|
||||
// This allows you to modify the context before passing it to a ReaderResult computation.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// Local is particularly useful for:
|
||||
// - Adding values to the context
|
||||
// - Setting timeouts or deadlines
|
||||
// - Modifying context metadata
|
||||
//
|
||||
// The function f returns both a new context and a CancelFunc. The CancelFunc is automatically
|
||||
// called (via defer) after the ReaderResult computation completes to ensure proper cleanup.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The result type (unchanged)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the context, returning a new context and CancelFunc
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a ReaderResult[A] and returns a ReaderResult[A]
|
||||
func Local[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
|
||||
return func(rr ReaderResult[A]) ReaderResult[A] {
|
||||
return func(ctx context.Context) Result[A] {
|
||||
otherCtx, otherCancel := f(ctx)
|
||||
defer otherCancel()
|
||||
return rr(otherCtx)
|
||||
}
|
||||
}
|
||||
}
|
||||
92
v2/context/readerresult/profunctor_test.go
Normal file
92
v2/context/readerresult/profunctor_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
R "github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestPromapBasic tests basic Promap functionality
|
||||
func TestPromapBasic(t *testing.T) {
|
||||
t.Run("transform both context and output", func(t *testing.T) {
|
||||
getValue := func(ctx context.Context) Result[int] {
|
||||
if v := ctx.Value("key"); v != nil {
|
||||
return R.Of(v.(int))
|
||||
}
|
||||
return R.Of(0)
|
||||
}
|
||||
|
||||
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
newCtx := context.WithValue(ctx, "key", 42)
|
||||
return newCtx, func() {}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap(addKey, toString)(getValue)
|
||||
result := adapted(context.Background())
|
||||
|
||||
assert.Equal(t, R.Of("42"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestContramapBasic tests basic Contramap functionality
|
||||
func TestContramapBasic(t *testing.T) {
|
||||
t.Run("context transformation", func(t *testing.T) {
|
||||
getValue := func(ctx context.Context) Result[int] {
|
||||
if v := ctx.Value("key"); v != nil {
|
||||
return R.Of(v.(int))
|
||||
}
|
||||
return R.Of(0)
|
||||
}
|
||||
|
||||
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
newCtx := context.WithValue(ctx, "key", 100)
|
||||
return newCtx, func() {}
|
||||
}
|
||||
|
||||
adapted := Contramap[int](addKey)(getValue)
|
||||
result := adapted(context.Background())
|
||||
|
||||
assert.Equal(t, R.Of(100), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalBasic tests basic Local functionality
|
||||
func TestLocalBasic(t *testing.T) {
|
||||
t.Run("adds value to context", func(t *testing.T) {
|
||||
getValue := func(ctx context.Context) Result[string] {
|
||||
if v := ctx.Value("user"); v != nil {
|
||||
return R.Of(v.(string))
|
||||
}
|
||||
return R.Of("unknown")
|
||||
}
|
||||
|
||||
addUser := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
newCtx := context.WithValue(ctx, "user", "Alice")
|
||||
return newCtx, func() {}
|
||||
}
|
||||
|
||||
adapted := Local[string](addUser)(getValue)
|
||||
result := adapted(context.Background())
|
||||
|
||||
assert.Equal(t, R.Of("Alice"), result)
|
||||
})
|
||||
}
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
// TestFirstMonoid tests the FirstMonoid implementation
|
||||
func TestFirstMonoid(t *testing.T) {
|
||||
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
|
||||
m := FirstMonoid[error, int](zero)
|
||||
m := FirstMonoid(zero)
|
||||
|
||||
t.Run("both Right values - returns first", func(t *testing.T) {
|
||||
result := m.Concat(Right[error](2), Right[error](3))
|
||||
@@ -94,7 +94,7 @@ func TestFirstMonoid(t *testing.T) {
|
||||
|
||||
t.Run("with strings", func(t *testing.T) {
|
||||
zeroStr := func() Either[error, string] { return Left[string](errors.New("empty")) }
|
||||
strMonoid := FirstMonoid[error, string](zeroStr)
|
||||
strMonoid := FirstMonoid(zeroStr)
|
||||
|
||||
result := strMonoid.Concat(Right[error]("first"), Right[error]("second"))
|
||||
assert.Equal(t, Right[error]("first"), result)
|
||||
@@ -107,7 +107,7 @@ func TestFirstMonoid(t *testing.T) {
|
||||
// TestLastMonoid tests the LastMonoid implementation
|
||||
func TestLastMonoid(t *testing.T) {
|
||||
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
|
||||
m := LastMonoid[error, int](zero)
|
||||
m := LastMonoid(zero)
|
||||
|
||||
t.Run("both Right values - returns last", func(t *testing.T) {
|
||||
result := m.Concat(Right[error](2), Right[error](3))
|
||||
@@ -176,7 +176,7 @@ func TestLastMonoid(t *testing.T) {
|
||||
|
||||
t.Run("with strings", func(t *testing.T) {
|
||||
zeroStr := func() Either[error, string] { return Left[string](errors.New("empty")) }
|
||||
strMonoid := LastMonoid[error, string](zeroStr)
|
||||
strMonoid := LastMonoid(zeroStr)
|
||||
|
||||
result := strMonoid.Concat(Right[error]("first"), Right[error]("second"))
|
||||
assert.Equal(t, Right[error]("second"), result)
|
||||
@@ -189,8 +189,8 @@ func TestLastMonoid(t *testing.T) {
|
||||
// TestFirstMonoidVsAltMonoid verifies FirstMonoid and AltMonoid have the same behavior
|
||||
func TestFirstMonoidVsAltMonoid(t *testing.T) {
|
||||
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
|
||||
firstMonoid := FirstMonoid[error, int](zero)
|
||||
altMonoid := AltMonoid[error, int](zero)
|
||||
firstMonoid := FirstMonoid(zero)
|
||||
altMonoid := AltMonoid(zero)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
@@ -223,8 +223,8 @@ func TestFirstMonoidVsAltMonoid(t *testing.T) {
|
||||
// TestFirstMonoidVsLastMonoid verifies the difference between FirstMonoid and LastMonoid
|
||||
func TestFirstMonoidVsLastMonoid(t *testing.T) {
|
||||
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
|
||||
firstMonoid := FirstMonoid[error, int](zero)
|
||||
lastMonoid := LastMonoid[error, int](zero)
|
||||
firstMonoid := FirstMonoid(zero)
|
||||
lastMonoid := LastMonoid(zero)
|
||||
|
||||
t.Run("both Right - different results", func(t *testing.T) {
|
||||
firstResult := firstMonoid.Concat(Right[error](1), Right[error](2))
|
||||
@@ -279,7 +279,7 @@ func TestFirstMonoidVsLastMonoid(t *testing.T) {
|
||||
func TestMonoidLaws(t *testing.T) {
|
||||
t.Run("FirstMonoid laws", func(t *testing.T) {
|
||||
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
|
||||
m := FirstMonoid[error, int](zero)
|
||||
m := FirstMonoid(zero)
|
||||
|
||||
a := Right[error](1)
|
||||
b := Right[error](2)
|
||||
@@ -301,7 +301,7 @@ func TestMonoidLaws(t *testing.T) {
|
||||
|
||||
t.Run("LastMonoid laws", func(t *testing.T) {
|
||||
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
|
||||
m := LastMonoid[error, int](zero)
|
||||
m := LastMonoid(zero)
|
||||
|
||||
a := Right[error](1)
|
||||
b := Right[error](2)
|
||||
@@ -323,7 +323,7 @@ func TestMonoidLaws(t *testing.T) {
|
||||
|
||||
t.Run("FirstMonoid laws with Left values", func(t *testing.T) {
|
||||
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
|
||||
m := FirstMonoid[error, int](zero)
|
||||
m := FirstMonoid(zero)
|
||||
|
||||
a := Left[int](errors.New("err1"))
|
||||
b := Left[int](errors.New("err2"))
|
||||
@@ -337,7 +337,7 @@ func TestMonoidLaws(t *testing.T) {
|
||||
|
||||
t.Run("LastMonoid laws with Left values", func(t *testing.T) {
|
||||
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
|
||||
m := LastMonoid[error, int](zero)
|
||||
m := LastMonoid(zero)
|
||||
|
||||
a := Left[int](errors.New("err1"))
|
||||
b := Left[int](errors.New("err2"))
|
||||
@@ -354,7 +354,7 @@ func TestMonoidLaws(t *testing.T) {
|
||||
func TestMonoidEdgeCases(t *testing.T) {
|
||||
t.Run("FirstMonoid with empty concatenations", func(t *testing.T) {
|
||||
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
|
||||
m := FirstMonoid[error, int](zero)
|
||||
m := FirstMonoid(zero)
|
||||
|
||||
// Empty with empty
|
||||
result := m.Concat(m.Empty(), m.Empty())
|
||||
@@ -363,7 +363,7 @@ func TestMonoidEdgeCases(t *testing.T) {
|
||||
|
||||
t.Run("LastMonoid with empty concatenations", func(t *testing.T) {
|
||||
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
|
||||
m := LastMonoid[error, int](zero)
|
||||
m := LastMonoid(zero)
|
||||
|
||||
// Empty with empty
|
||||
result := m.Concat(m.Empty(), m.Empty())
|
||||
@@ -372,7 +372,7 @@ func TestMonoidEdgeCases(t *testing.T) {
|
||||
|
||||
t.Run("FirstMonoid chain of operations", func(t *testing.T) {
|
||||
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
|
||||
m := FirstMonoid[error, int](zero)
|
||||
m := FirstMonoid(zero)
|
||||
|
||||
// Chain multiple operations
|
||||
result := m.Concat(
|
||||
@@ -387,7 +387,7 @@ func TestMonoidEdgeCases(t *testing.T) {
|
||||
|
||||
t.Run("LastMonoid chain of operations", func(t *testing.T) {
|
||||
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
|
||||
m := LastMonoid[error, int](zero)
|
||||
m := LastMonoid(zero)
|
||||
|
||||
// Chain multiple operations
|
||||
result := m.Concat(
|
||||
|
||||
@@ -20,6 +20,8 @@ package eq
|
||||
// by mapping the input type. It's particularly useful for comparing complex types by
|
||||
// extracting comparable fields.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// The name "contramap" comes from category theory, where it represents a contravariant
|
||||
// functor. Unlike regular map (covariant), which transforms the output, contramap
|
||||
// transforms the input in the opposite direction.
|
||||
|
||||
@@ -21,54 +21,261 @@ import (
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
)
|
||||
|
||||
// MonadAp applies a function to a value in the Identity monad context.
|
||||
// Since Identity has no computational context, this is just function application.
|
||||
//
|
||||
// This is the uncurried version of Ap.
|
||||
//
|
||||
// Implements the Fantasy Land Apply specification:
|
||||
// https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#apply
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := identity.MonadAp(func(n int) int { return n * 2 }, 21)
|
||||
// // result is 42
|
||||
func MonadAp[B, A any](fab func(A) B, fa A) B {
|
||||
return fab(fa)
|
||||
}
|
||||
|
||||
// Ap applies a wrapped function to a wrapped value.
|
||||
// Returns a function that takes a function and applies the value to it.
|
||||
//
|
||||
// This is the curried version of MonadAp, useful for composition with Pipe.
|
||||
//
|
||||
// Implements the Fantasy Land Apply specification:
|
||||
// https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#apply
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import F "github.com/IBM/fp-go/v2/function"
|
||||
//
|
||||
// double := func(n int) int { return n * 2 }
|
||||
// result := F.Pipe1(double, identity.Ap[int](21))
|
||||
// // result is 42
|
||||
func Ap[B, A any](fa A) Operator[func(A) B, B] {
|
||||
return function.Bind2nd(MonadAp[B, A], fa)
|
||||
}
|
||||
|
||||
// MonadMap transforms a value using a function in the Identity monad context.
|
||||
// Since Identity has no computational context, this is just function application.
|
||||
//
|
||||
// This is the uncurried version of Map.
|
||||
//
|
||||
// Implements the Fantasy Land Functor specification:
|
||||
// https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#functor
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := identity.MonadMap(21, func(n int) int { return n * 2 })
|
||||
// // result is 42
|
||||
func MonadMap[A, B any](fa A, f func(A) B) B {
|
||||
return f(fa)
|
||||
}
|
||||
|
||||
// Map transforms a value using a function.
|
||||
// Returns the function itself since Identity adds no context.
|
||||
//
|
||||
// This is the curried version of MonadMap, useful for composition with Pipe.
|
||||
//
|
||||
// Implements the Fantasy Land Functor specification:
|
||||
// https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#functor
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import F "github.com/IBM/fp-go/v2/function"
|
||||
//
|
||||
// result := F.Pipe1(21, identity.Map(func(n int) int { return n * 2 }))
|
||||
// // result is 42
|
||||
func Map[A, B any](f func(A) B) Operator[A, B] {
|
||||
return f
|
||||
}
|
||||
|
||||
// MonadMapTo replaces a value with a constant, ignoring the input.
|
||||
//
|
||||
// This is the uncurried version of MapTo.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := identity.MonadMapTo("ignored", 42)
|
||||
// // result is 42
|
||||
func MonadMapTo[A, B any](_ A, b B) B {
|
||||
return b
|
||||
}
|
||||
|
||||
// MapTo replaces any value with a constant value.
|
||||
// Returns a function that ignores its input and returns the constant.
|
||||
//
|
||||
// This is the curried version of MonadMapTo, useful for composition with Pipe.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import F "github.com/IBM/fp-go/v2/function"
|
||||
//
|
||||
// result := F.Pipe1("ignored", identity.MapTo[string](42))
|
||||
// // result is 42
|
||||
func MapTo[A, B any](b B) func(A) B {
|
||||
return function.Constant1[A](b)
|
||||
}
|
||||
|
||||
// Of wraps a value in the Identity monad.
|
||||
// Since Identity has no computational context, this is just the identity function.
|
||||
//
|
||||
// This is the Pointed/Applicative "pure" operation.
|
||||
//
|
||||
// Implements the Fantasy Land Applicative specification:
|
||||
// https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#applicative
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// value := identity.Of(42)
|
||||
// // value is 42
|
||||
//
|
||||
//go:inline
|
||||
func Of[A any](a A) A {
|
||||
return a
|
||||
}
|
||||
|
||||
// MonadChain applies a Kleisli arrow to a value in the Identity monad context.
|
||||
// Since Identity has no computational context, this is just function application.
|
||||
//
|
||||
// This is the uncurried version of Chain, also known as "bind" or "flatMap".
|
||||
//
|
||||
// Implements the Fantasy Land Chain specification:
|
||||
// https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#chain
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := identity.MonadChain(21, func(n int) int { return n * 2 })
|
||||
// // result is 42
|
||||
func MonadChain[A, B any](ma A, f Kleisli[A, B]) B {
|
||||
return f(ma)
|
||||
}
|
||||
|
||||
// Chain applies a Kleisli arrow to a value.
|
||||
// Returns the function itself since Identity adds no context.
|
||||
//
|
||||
// This is the curried version of MonadChain, also known as "bind" or "flatMap".
|
||||
// Useful for composition with Pipe.
|
||||
//
|
||||
// Implements the Fantasy Land Chain specification:
|
||||
// https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#chain
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import F "github.com/IBM/fp-go/v2/function"
|
||||
//
|
||||
// result := F.Pipe1(21, identity.Chain(func(n int) int { return n * 2 }))
|
||||
// // result is 42
|
||||
//
|
||||
//go:inline
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||
return f
|
||||
}
|
||||
|
||||
// MonadChainFirst executes a computation for its effect but returns the original value.
|
||||
// Useful for side effects like logging while preserving the original value.
|
||||
//
|
||||
// This is the uncurried version of ChainFirst.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := identity.MonadChainFirst(42, func(n int) string {
|
||||
// fmt.Printf("Value: %d\n", n)
|
||||
// return "logged"
|
||||
// })
|
||||
// // result is 42 (original value preserved)
|
||||
func MonadChainFirst[A, B any](fa A, f Kleisli[A, B]) A {
|
||||
return chain.MonadChainFirst(MonadChain[A, A], MonadMap[B, A], fa, f)
|
||||
}
|
||||
|
||||
// ChainFirst executes a computation for its effect but returns the original value.
|
||||
// Useful for side effects like logging while preserving the original value.
|
||||
//
|
||||
// This is the curried version of MonadChainFirst, useful for composition with Pipe.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import F "github.com/IBM/fp-go/v2/function"
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// 42,
|
||||
// identity.ChainFirst(func(n int) string {
|
||||
// fmt.Printf("Value: %d\n", n)
|
||||
// return "logged"
|
||||
// }),
|
||||
// )
|
||||
// // result is 42 (original value preserved)
|
||||
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
||||
return chain.ChainFirst(Chain[A, A], Map[B, A], f)
|
||||
}
|
||||
|
||||
// MonadFlap applies a value to a function, flipping the normal application order.
|
||||
// Instead of applying a function to a value, it applies a value to a function.
|
||||
//
|
||||
// This is the uncurried version of Flap.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// double := func(n int) int { return n * 2 }
|
||||
// result := identity.MonadFlap(double, 21)
|
||||
// // result is 42
|
||||
func MonadFlap[B, A any](fab func(A) B, a A) B {
|
||||
return functor.MonadFlap(MonadMap[func(A) B, B], fab, a)
|
||||
}
|
||||
|
||||
// Flap applies a value to a function, flipping the normal application order.
|
||||
// Returns a function that takes a function and applies the value to it.
|
||||
//
|
||||
// This is the curried version of MonadFlap, useful for composition with Pipe.
|
||||
// Useful when you have a value and want to apply it to multiple functions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import F "github.com/IBM/fp-go/v2/function"
|
||||
//
|
||||
// double := func(n int) int { return n * 2 }
|
||||
// result := F.Pipe1(double, identity.Flap[int](21))
|
||||
// // result is 42
|
||||
//
|
||||
//go:inline
|
||||
func Flap[B, A any](a A) Operator[func(A) B, B] {
|
||||
return functor.Flap(Map[func(A) B, B], a)
|
||||
}
|
||||
|
||||
// Extract extracts the value from the Identity monad.
|
||||
// Since Identity has no computational context, this is just the identity function.
|
||||
//
|
||||
// This is the Comonad "extract" operation.
|
||||
//
|
||||
// Implements the Fantasy Land Comonad specification:
|
||||
// https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#comonad
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// value := identity.Extract(42)
|
||||
// // value is 42
|
||||
//
|
||||
//go:inline
|
||||
func Extract[A any](a A) A {
|
||||
return a
|
||||
}
|
||||
|
||||
// Extend extends a computation over the Identity monad.
|
||||
// Since Identity has no computational context, this is just function application.
|
||||
//
|
||||
// This is the Comonad "extend" operation, also known as "cobind".
|
||||
//
|
||||
// Implements the Fantasy Land Extend specification:
|
||||
// https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#extend
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import F "github.com/IBM/fp-go/v2/function"
|
||||
//
|
||||
// result := F.Pipe1(21, identity.Extend(func(n int) int { return n * 2 }))
|
||||
// // result is 42
|
||||
//
|
||||
//go:inline
|
||||
func Extend[A, B any](f func(A) B) Operator[A, B] {
|
||||
return f
|
||||
}
|
||||
|
||||
@@ -723,3 +723,99 @@ func TestTraverseTuple10(t *testing.T) {
|
||||
assert.Equal(t, T.MakeTuple10(2, 4, 6, 8, 10, 12, 14, 16, 18, 20), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExtract(t *testing.T) {
|
||||
t.Run("extracts int value", func(t *testing.T) {
|
||||
result := Extract(42)
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
|
||||
t.Run("extracts string value", func(t *testing.T) {
|
||||
result := Extract("hello")
|
||||
assert.Equal(t, "hello", result)
|
||||
})
|
||||
|
||||
t.Run("extracts struct value", func(t *testing.T) {
|
||||
type Person struct{ Name string }
|
||||
p := Person{Name: "Alice"}
|
||||
result := Extract(p)
|
||||
assert.Equal(t, p, result)
|
||||
})
|
||||
|
||||
t.Run("extracts pointer value", func(t *testing.T) {
|
||||
value := 100
|
||||
ptr := &value
|
||||
result := Extract(ptr)
|
||||
assert.Equal(t, ptr, result)
|
||||
assert.Equal(t, 100, *result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExtend(t *testing.T) {
|
||||
t.Run("extends with transformation", func(t *testing.T) {
|
||||
result := F.Pipe1(21, Extend(utils.Double))
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
|
||||
t.Run("extends with type change", func(t *testing.T) {
|
||||
result := F.Pipe1(42, Extend(S.Format[int]("Number: %d")))
|
||||
assert.Equal(t, "Number: 42", result)
|
||||
})
|
||||
|
||||
t.Run("chains multiple extends", func(t *testing.T) {
|
||||
result := F.Pipe2(
|
||||
5,
|
||||
Extend(N.Mul(2)),
|
||||
Extend(N.Add(10)),
|
||||
)
|
||||
assert.Equal(t, 20, result)
|
||||
})
|
||||
|
||||
t.Run("extends with complex computation", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
10,
|
||||
Extend(func(n int) string {
|
||||
doubled := n * 2
|
||||
return fmt.Sprintf("Result: %d", doubled)
|
||||
}),
|
||||
)
|
||||
assert.Equal(t, "Result: 20", result)
|
||||
})
|
||||
}
|
||||
|
||||
// Test Comonad laws
|
||||
func TestComonadLaws(t *testing.T) {
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
// Extract(Extend(f)(w)) === f(w)
|
||||
w := 42
|
||||
f := N.Mul(2)
|
||||
|
||||
left := Extract(F.Pipe1(w, Extend(f)))
|
||||
right := f(w)
|
||||
|
||||
assert.Equal(t, right, left)
|
||||
})
|
||||
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
// Extend(Extract)(w) === w
|
||||
w := 42
|
||||
|
||||
result := F.Pipe1(w, Extend(Extract[int]))
|
||||
|
||||
assert.Equal(t, w, result)
|
||||
})
|
||||
|
||||
t.Run("associativity", func(t *testing.T) {
|
||||
// Extend(f)(Extend(g)(w)) === Extend(x => f(Extend(g)(x)))(w)
|
||||
w := 5
|
||||
f := N.Mul(2)
|
||||
g := N.Add(10)
|
||||
|
||||
left := F.Pipe2(w, Extend(g), Extend(f))
|
||||
right := F.Pipe1(w, Extend(func(x int) int {
|
||||
return f(F.Pipe1(x, Extend(g)))
|
||||
}))
|
||||
|
||||
assert.Equal(t, right, left)
|
||||
})
|
||||
}
|
||||
|
||||
59
v2/idiomatic/context/readerresult/profunctor.go
Normal file
59
v2/idiomatic/context/readerresult/profunctor.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a ReaderResult.
|
||||
// It applies f to the input context (contravariantly) and g to the output value (covariantly).
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// This operation allows you to:
|
||||
// - Adapt the context before passing it to the ReaderResult (via f)
|
||||
// - Transform the success value after the computation completes (via g)
|
||||
//
|
||||
// The error type is fixed as error and remains unchanged through the transformation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The original success type produced by the ReaderResult
|
||||
// - B: The new output success type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the input context, returning a new context and cancel function (contravariant)
|
||||
// - g: Function to transform the output success value from A to B (covariant)
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a ReaderResult[A] and returns a ReaderResult[B]
|
||||
//
|
||||
//go:inline
|
||||
func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFunc), g func(A) B) Operator[A, B] {
|
||||
return function.Flow2(
|
||||
Local[A](f),
|
||||
Map(g),
|
||||
)
|
||||
}
|
||||
|
||||
// Contramap changes the value of the local context during the execution of a ReaderResult.
|
||||
// This is the contravariant functor operation that transforms the input context.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// Contramap is useful for adapting a ReaderResult to work with a modified context
|
||||
// by providing a function that creates a new context (and optional cancel function) from the current one.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type (unchanged)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the context, returning a new context and cancel function
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderResult[A] and returns a ReaderResult[A]
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[A any](f func(context.Context) (context.Context, context.CancelFunc)) Kleisli[ReaderResult[A], A] {
|
||||
return Local[A](f)
|
||||
}
|
||||
187
v2/idiomatic/context/readerresult/profunctor_test.go
Normal file
187
v2/idiomatic/context/readerresult/profunctor_test.go
Normal file
@@ -0,0 +1,187 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestPromapBasic tests basic Promap functionality
|
||||
func TestPromapBasic(t *testing.T) {
|
||||
t.Run("transform both context and output", func(t *testing.T) {
|
||||
// ReaderResult that reads a value from context
|
||||
getValue := func(ctx context.Context) (int, error) {
|
||||
if val := ctx.Value("port"); val != nil {
|
||||
return val.(int), nil
|
||||
}
|
||||
return 0, fmt.Errorf("port not found")
|
||||
}
|
||||
|
||||
// Transform context to add a value and int to string
|
||||
addPort := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
return context.WithValue(ctx, "port", 8080), func() {}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap(addPort, toString)(getValue)
|
||||
result, err := adapted(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "8080", result)
|
||||
})
|
||||
|
||||
t.Run("handles error case", func(t *testing.T) {
|
||||
// ReaderResult that returns an error
|
||||
getError := func(ctx context.Context) (int, error) {
|
||||
return 0, fmt.Errorf("error occurred")
|
||||
}
|
||||
|
||||
addPort := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
return context.WithValue(ctx, "port", 8080), func() {}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap(addPort, toString)(getError)
|
||||
_, err := adapted(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "error occurred", err.Error())
|
||||
})
|
||||
|
||||
t.Run("context transformation with cancellation", func(t *testing.T) {
|
||||
getValue := func(ctx context.Context) (string, error) {
|
||||
if val := ctx.Value("key"); val != nil {
|
||||
return val.(string), nil
|
||||
}
|
||||
return "", fmt.Errorf("key not found")
|
||||
}
|
||||
|
||||
addValue := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
return context.WithValue(ctx, "key", "value"), cancel
|
||||
}
|
||||
toUpper := func(s string) string {
|
||||
return "UPPER_" + s
|
||||
}
|
||||
|
||||
adapted := Promap(addValue, toUpper)(getValue)
|
||||
result, err := adapted(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "UPPER_value", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestContramapBasic tests basic Contramap functionality
|
||||
func TestContramapBasic(t *testing.T) {
|
||||
t.Run("context adaptation", func(t *testing.T) {
|
||||
// ReaderResult that reads from context
|
||||
getPort := func(ctx context.Context) (int, error) {
|
||||
if val := ctx.Value("port"); val != nil {
|
||||
return val.(int), nil
|
||||
}
|
||||
return 0, fmt.Errorf("port not found")
|
||||
}
|
||||
|
||||
// Adapt context to add port value
|
||||
addPort := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
return context.WithValue(ctx, "port", 9000), func() {}
|
||||
}
|
||||
|
||||
adapted := Contramap[int](addPort)(getPort)
|
||||
result, err := adapted(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 9000, result)
|
||||
})
|
||||
|
||||
t.Run("preserves error", func(t *testing.T) {
|
||||
getError := func(ctx context.Context) (int, error) {
|
||||
return 0, fmt.Errorf("config error")
|
||||
}
|
||||
|
||||
addPort := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
return context.WithValue(ctx, "port", 9000), func() {}
|
||||
}
|
||||
|
||||
adapted := Contramap[int](addPort)(getError)
|
||||
_, err := adapted(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "config error", err.Error())
|
||||
})
|
||||
|
||||
t.Run("multiple context values", func(t *testing.T) {
|
||||
getValues := func(ctx context.Context) (string, error) {
|
||||
host := ctx.Value("host")
|
||||
port := ctx.Value("port")
|
||||
if host != nil && port != nil {
|
||||
return fmt.Sprintf("%s:%d", host, port), nil
|
||||
}
|
||||
return "", fmt.Errorf("missing values")
|
||||
}
|
||||
|
||||
addValues := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
ctx = context.WithValue(ctx, "host", "localhost")
|
||||
ctx = context.WithValue(ctx, "port", 8080)
|
||||
return ctx, func() {}
|
||||
}
|
||||
|
||||
adapted := Contramap[string](addValues)(getValues)
|
||||
result, err := adapted(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "localhost:8080", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapComposition tests that Promap can be composed
|
||||
func TestPromapComposition(t *testing.T) {
|
||||
t.Run("compose two Promap transformations", func(t *testing.T) {
|
||||
reader := func(ctx context.Context) (int, error) {
|
||||
if val := ctx.Value("value"); val != nil {
|
||||
return val.(int), nil
|
||||
}
|
||||
return 0, fmt.Errorf("value not found")
|
||||
}
|
||||
|
||||
f1 := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
return context.WithValue(ctx, "value", 5), func() {}
|
||||
}
|
||||
g1 := N.Mul(2)
|
||||
|
||||
f2 := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
return ctx, func() {}
|
||||
}
|
||||
g2 := N.Add(10)
|
||||
|
||||
// Apply two Promap transformations
|
||||
step1 := Promap(f1, g1)(reader)
|
||||
step2 := Promap(f2, g2)(step1)
|
||||
|
||||
result, err := step2(context.Background())
|
||||
|
||||
// (5 * 2) + 10 = 20
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 20, result)
|
||||
})
|
||||
}
|
||||
74
v2/idiomatic/readerioresult/profunctor.go
Normal file
74
v2/idiomatic/readerioresult/profunctor.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a ReaderIOResult.
|
||||
// It applies f to the input environment (contravariantly) and g to the output value (covariantly).
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// This operation allows you to:
|
||||
// - Adapt the environment type before passing it to the ReaderIOResult (via f)
|
||||
// - Transform the success value after the IO effect completes (via g)
|
||||
//
|
||||
// The error type is fixed as error and remains unchanged through the transformation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The original environment type expected by the ReaderIOResult
|
||||
// - A: The original success type produced by the ReaderIOResult
|
||||
// - D: The new input environment type
|
||||
// - B: The new output success type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the input environment from D to E (contravariant)
|
||||
// - g: Function to transform the output success value from A to B (covariant)
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderIOResult[E, A] and returns a ReaderIOResult[D, B]
|
||||
//
|
||||
//go:inline
|
||||
func Promap[E, A, D, B any](f func(D) E, g func(A) B) Kleisli[D, ReaderIOResult[E, A], B] {
|
||||
return reader.Promap(f, ioresult.Map(g))
|
||||
}
|
||||
|
||||
// Contramap changes the value of the local environment during the execution of a ReaderIOResult.
|
||||
// This is the contravariant functor operation that transforms the input environment.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// Contramap is useful for adapting a ReaderIOResult to work with a different environment type
|
||||
// by providing a function that converts the new environment to the expected one.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type (unchanged)
|
||||
// - R2: The new input environment type
|
||||
// - R1: The original environment type expected by the ReaderIOResult
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the environment from R2 to R1
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderIOResult[R1, A] and returns a ReaderIOResult[R2, A]
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[A, R1, R2 any](f func(R2) R1) Kleisli[R2, ReaderIOResult[R1, A], A] {
|
||||
return Local[A](f)
|
||||
}
|
||||
199
v2/idiomatic/readerioresult/profunctor_test.go
Normal file
199
v2/idiomatic/readerioresult/profunctor_test.go
Normal file
@@ -0,0 +1,199 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type SimpleConfig struct {
|
||||
Port int
|
||||
}
|
||||
|
||||
type DetailedConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
// TestPromapBasic tests basic Promap functionality
|
||||
func TestPromapBasic(t *testing.T) {
|
||||
t.Run("transform both input and output", func(t *testing.T) {
|
||||
// ReaderIOResult that reads port from SimpleConfig
|
||||
getPort := func(c SimpleConfig) func() (int, error) {
|
||||
return func() (int, error) {
|
||||
return c.Port, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Transform DetailedConfig to SimpleConfig and int to string
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap(simplify, toString)(getPort)
|
||||
result, err := adapted(DetailedConfig{Host: "localhost", Port: 8080})()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "8080", result)
|
||||
})
|
||||
|
||||
t.Run("handles error case", func(t *testing.T) {
|
||||
// ReaderIOResult that returns an error
|
||||
getError := func(c SimpleConfig) func() (int, error) {
|
||||
return func() (int, error) {
|
||||
return 0, fmt.Errorf("error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap(simplify, toString)(getError)
|
||||
_, err := adapted(DetailedConfig{Host: "localhost", Port: 8080})()
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "error occurred", err.Error())
|
||||
})
|
||||
}
|
||||
|
||||
// TestContramapBasic tests basic Contramap functionality
|
||||
func TestContramapBasic(t *testing.T) {
|
||||
t.Run("environment adaptation", func(t *testing.T) {
|
||||
// ReaderIOResult that reads from SimpleConfig
|
||||
getPort := func(c SimpleConfig) func() (int, error) {
|
||||
return func() (int, error) {
|
||||
return c.Port, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Adapt to work with DetailedConfig
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
|
||||
adapted := Contramap[int](simplify)(getPort)
|
||||
result, err := adapted(DetailedConfig{Host: "localhost", Port: 9000})()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 9000, result)
|
||||
})
|
||||
|
||||
t.Run("preserves error", func(t *testing.T) {
|
||||
getError := func(c SimpleConfig) func() (int, error) {
|
||||
return func() (int, error) {
|
||||
return 0, fmt.Errorf("config error")
|
||||
}
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
|
||||
adapted := Contramap[int](simplify)(getError)
|
||||
_, err := adapted(DetailedConfig{Host: "localhost", Port: 9000})()
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "config error", err.Error())
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapWithIO tests Promap with actual IO effects
|
||||
func TestPromapWithIO(t *testing.T) {
|
||||
t.Run("transform IO result", func(t *testing.T) {
|
||||
counter := 0
|
||||
|
||||
// ReaderIOResult with side effect
|
||||
getPortWithEffect := func(c SimpleConfig) func() (int, error) {
|
||||
return func() (int, error) {
|
||||
counter++
|
||||
return c.Port, nil
|
||||
}
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap(simplify, toString)(getPortWithEffect)
|
||||
result, err := adapted(DetailedConfig{Host: "localhost", Port: 8080})()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "8080", result)
|
||||
assert.Equal(t, 1, counter) // Side effect occurred
|
||||
})
|
||||
|
||||
t.Run("side effect occurs even on error", func(t *testing.T) {
|
||||
counter := 0
|
||||
|
||||
getErrorWithEffect := func(c SimpleConfig) func() (int, error) {
|
||||
return func() (int, error) {
|
||||
counter++
|
||||
return 0, fmt.Errorf("io error")
|
||||
}
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap(simplify, toString)(getErrorWithEffect)
|
||||
_, err := adapted(DetailedConfig{Host: "localhost", Port: 8080})()
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 1, counter) // Side effect occurred before error
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapComposition tests that Promap can be composed
|
||||
func TestPromapComposition(t *testing.T) {
|
||||
t.Run("compose two Promap transformations", func(t *testing.T) {
|
||||
type Config1 struct{ Value int }
|
||||
type Config2 struct{ Value int }
|
||||
type Config3 struct{ Value int }
|
||||
|
||||
reader := func(c Config1) func() (int, error) {
|
||||
return func() (int, error) {
|
||||
return c.Value, nil
|
||||
}
|
||||
}
|
||||
|
||||
f1 := func(c2 Config2) Config1 { return Config1{Value: c2.Value} }
|
||||
g1 := N.Mul(2)
|
||||
|
||||
f2 := func(c3 Config3) Config2 { return Config2{Value: c3.Value} }
|
||||
g2 := N.Add(10)
|
||||
|
||||
// Apply two Promap transformations
|
||||
step1 := Promap(f1, g1)(reader)
|
||||
step2 := Promap(f2, g2)(step1)
|
||||
|
||||
result, err := step2(Config3{Value: 5})()
|
||||
|
||||
// (5 * 2) + 10 = 20
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 20, result)
|
||||
})
|
||||
}
|
||||
76
v2/idiomatic/readerresult/profunctor.go
Normal file
76
v2/idiomatic/readerresult/profunctor.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerresult
|
||||
|
||||
import "github.com/IBM/fp-go/v2/idiomatic/result"
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a ReaderResult.
|
||||
// It applies f to the input environment (contravariantly) and g to the output value (covariantly).
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// This operation allows you to:
|
||||
// - Adapt the environment type before passing it to the ReaderResult (via f)
|
||||
// - Transform the success value after the computation completes (via g)
|
||||
//
|
||||
// The error type is fixed as error and remains unchanged through the transformation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The original environment type expected by the ReaderResult
|
||||
// - A: The original success type produced by the ReaderResult
|
||||
// - D: The new input environment type
|
||||
// - B: The new output success type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the input environment from D to E (contravariant)
|
||||
// - g: Function to transform the output success value from A to B (covariant)
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderResult[E, A] and returns a ReaderResult[D, B]
|
||||
//
|
||||
//go:inline
|
||||
func Promap[E, A, D, B any](f func(D) E, g func(A) B) Kleisli[D, ReaderResult[E, A], B] {
|
||||
mp := result.Map(g)
|
||||
return func(rr ReaderResult[E, A]) ReaderResult[D, B] {
|
||||
return func(d D) (B, error) {
|
||||
return mp(rr(f(d)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Contramap changes the value of the local environment during the execution of a ReaderResult.
|
||||
// This is the contravariant functor operation that transforms the input environment.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// Contramap is useful for adapting a ReaderResult to work with a different environment type
|
||||
// by providing a function that converts the new environment to the expected one.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type (unchanged)
|
||||
// - R2: The new input environment type
|
||||
// - R1: The original environment type expected by the ReaderResult
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the environment from R2 to R1
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderResult[R1, A] and returns a ReaderResult[R2, A]
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[A, R1, R2 any](f func(R2) R1) Kleisli[R2, ReaderResult[R1, A], A] {
|
||||
return Local[A](f)
|
||||
}
|
||||
238
v2/idiomatic/readerresult/profunctor_test.go
Normal file
238
v2/idiomatic/readerresult/profunctor_test.go
Normal file
@@ -0,0 +1,238 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerresult
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
R "github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type SimpleConfig struct {
|
||||
Port int
|
||||
}
|
||||
|
||||
type DetailedConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
// TestPromapBasic tests basic Promap functionality
|
||||
func TestPromapBasic(t *testing.T) {
|
||||
t.Run("transform both input and output", func(t *testing.T) {
|
||||
// ReaderResult that reads port from SimpleConfig
|
||||
getPort := func(c SimpleConfig) (int, error) {
|
||||
return c.Port, nil
|
||||
}
|
||||
|
||||
// Transform DetailedConfig to SimpleConfig and int to string
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap(simplify, toString)(getPort)
|
||||
result, err := adapted(DetailedConfig{Host: "localhost", Port: 8080})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "8080", result)
|
||||
})
|
||||
|
||||
t.Run("handles error case", func(t *testing.T) {
|
||||
// ReaderResult that returns an error
|
||||
getError := func(c SimpleConfig) (int, error) {
|
||||
return 0, fmt.Errorf("error occurred")
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap(simplify, toString)(getError)
|
||||
_, err := adapted(DetailedConfig{Host: "localhost", Port: 8080})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "error occurred", err.Error())
|
||||
})
|
||||
|
||||
t.Run("environment transformation with complex types", func(t *testing.T) {
|
||||
type Database struct {
|
||||
ConnectionString string
|
||||
}
|
||||
type AppConfig struct {
|
||||
DB Database
|
||||
}
|
||||
|
||||
getConnection := func(db Database) (string, error) {
|
||||
if db.ConnectionString == "" {
|
||||
return "", fmt.Errorf("empty connection string")
|
||||
}
|
||||
return db.ConnectionString, nil
|
||||
}
|
||||
|
||||
extractDB := func(cfg AppConfig) Database {
|
||||
return cfg.DB
|
||||
}
|
||||
addPrefix := func(s string) string {
|
||||
return "postgres://" + s
|
||||
}
|
||||
|
||||
adapted := Promap(extractDB, addPrefix)(getConnection)
|
||||
result, err := adapted(AppConfig{DB: Database{ConnectionString: "localhost:5432"}})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "postgres://localhost:5432", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestContramapBasic tests basic Contramap functionality
|
||||
func TestContramapBasic(t *testing.T) {
|
||||
t.Run("environment adaptation", func(t *testing.T) {
|
||||
// ReaderResult that reads from SimpleConfig
|
||||
getPort := func(c SimpleConfig) (int, error) {
|
||||
return c.Port, nil
|
||||
}
|
||||
|
||||
// Adapt to work with DetailedConfig
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
|
||||
adapted := Contramap[int](simplify)(getPort)
|
||||
result, err := adapted(DetailedConfig{Host: "localhost", Port: 9000})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 9000, result)
|
||||
})
|
||||
|
||||
t.Run("preserves error", func(t *testing.T) {
|
||||
getError := func(c SimpleConfig) (int, error) {
|
||||
return 0, fmt.Errorf("config error")
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
|
||||
adapted := Contramap[int](simplify)(getError)
|
||||
_, err := adapted(DetailedConfig{Host: "localhost", Port: 9000})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "config error", err.Error())
|
||||
})
|
||||
|
||||
t.Run("multiple field extraction", func(t *testing.T) {
|
||||
type FullConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
Protocol string
|
||||
}
|
||||
|
||||
getURL := func(c DetailedConfig) (string, error) {
|
||||
return fmt.Sprintf("%s:%d", c.Host, c.Port), nil
|
||||
}
|
||||
|
||||
extractHostPort := func(fc FullConfig) DetailedConfig {
|
||||
return DetailedConfig{Host: fc.Host, Port: fc.Port}
|
||||
}
|
||||
|
||||
adapted := Contramap[string](extractHostPort)(getURL)
|
||||
result, err := adapted(FullConfig{Host: "example.com", Port: 443, Protocol: "https"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "example.com:443", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapComposition tests that Promap can be composed
|
||||
func TestPromapComposition(t *testing.T) {
|
||||
t.Run("compose two Promap transformations", func(t *testing.T) {
|
||||
type Config1 struct{ Value int }
|
||||
type Config2 struct{ Value int }
|
||||
type Config3 struct{ Value int }
|
||||
|
||||
reader := func(c Config1) (int, error) {
|
||||
return c.Value, nil
|
||||
}
|
||||
|
||||
f1 := func(c2 Config2) Config1 { return Config1{Value: c2.Value} }
|
||||
g1 := N.Mul(2)
|
||||
|
||||
f2 := func(c3 Config3) Config2 { return Config2{Value: c3.Value} }
|
||||
g2 := N.Add(10)
|
||||
|
||||
// Apply two Promap transformations
|
||||
step1 := Promap(f1, g1)(reader)
|
||||
step2 := Promap(f2, g2)(step1)
|
||||
|
||||
result, err := step2(Config3{Value: 5})
|
||||
|
||||
// (5 * 2) + 10 = 20
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 20, result)
|
||||
})
|
||||
|
||||
t.Run("compose Promap and Contramap", func(t *testing.T) {
|
||||
type Config1 struct{ Value int }
|
||||
type Config2 struct{ Value int }
|
||||
|
||||
reader := func(c Config1) (int, error) {
|
||||
return c.Value * 3, nil
|
||||
}
|
||||
|
||||
// First apply Contramap
|
||||
f1 := func(c2 Config2) Config1 { return Config1{Value: c2.Value} }
|
||||
step1 := Contramap[int](f1)(reader)
|
||||
|
||||
// Then apply Promap
|
||||
f2 := func(c2 Config2) Config2 { return c2 }
|
||||
g2 := func(n int) string { return fmt.Sprintf("result: %d", n) }
|
||||
step2 := Promap(f2, g2)(step1)
|
||||
|
||||
result, err := step2(Config2{Value: 7})
|
||||
|
||||
// 7 * 3 = 21
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "result: 21", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapIdentityLaws tests profunctor identity laws
|
||||
func TestPromapIdentityLaws(t *testing.T) {
|
||||
t.Run("identity law", func(t *testing.T) {
|
||||
// Promap with identity functions should be identity
|
||||
reader := func(c SimpleConfig) (int, error) {
|
||||
return c.Port, nil
|
||||
}
|
||||
|
||||
identity := R.Ask[SimpleConfig]()
|
||||
identityInt := R.Ask[int]()
|
||||
|
||||
adapted := Promap(identity, identityInt)(reader)
|
||||
|
||||
config := SimpleConfig{Port: 8080}
|
||||
result1, err1 := reader(config)
|
||||
result2, err2 := adapted(config)
|
||||
|
||||
assert.Equal(t, err1, err2)
|
||||
assert.Equal(t, result1, result2)
|
||||
})
|
||||
}
|
||||
@@ -501,7 +501,7 @@ func BiMap[R, A, B any](f Endomorphism[error], g func(A) B) Operator[R, A, B] {
|
||||
// rr := readerresult.Of[Config](42)
|
||||
// adapted := readerresult.Local[int](toConfig)(rr)
|
||||
// // adapted now accepts DB instead of Config
|
||||
func Local[A, R2, R1 any](f func(R2) R1) func(ReaderResult[R1, A]) ReaderResult[R2, A] {
|
||||
func Local[A, R1, R2 any](f func(R2) R1) func(ReaderResult[R1, A]) ReaderResult[R2, A] {
|
||||
return func(rr ReaderResult[R1, A]) ReaderResult[R2, A] {
|
||||
return func(r R2) (A, error) {
|
||||
return rr(f(r))
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
STR "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -267,7 +268,7 @@ func TestApV_ZeroValues(t *testing.T) {
|
||||
sg := makeErrorConcatSemigroup()
|
||||
apv := ApV[int, int](sg)
|
||||
|
||||
identity := func(x int) int { return x }
|
||||
identity := reader.Ask[int]()
|
||||
|
||||
value, verr := Right(0)
|
||||
fn, ferr := Right(identity)
|
||||
|
||||
@@ -240,7 +240,7 @@ func TestCopyFileChaining(t *testing.T) {
|
||||
// Chain two copy operations
|
||||
result := F.Pipe1(
|
||||
CopyFile(srcPath)(dst1Path),
|
||||
IOE.Chain[error](func(string) IOEither[error, string] {
|
||||
IOE.Chain(func(string) IOEither[error, string] {
|
||||
return CopyFile(dst1Path)(dst2Path)
|
||||
}),
|
||||
)()
|
||||
|
||||
@@ -141,7 +141,7 @@ func TestFilterOrElse_WithMap(t *testing.T) {
|
||||
onNegative := func(n int) string { return "negative number" }
|
||||
|
||||
filter := FilterOrElse(isPositive, onNegative)
|
||||
double := Map[string](func(n int) int { return n * 2 })
|
||||
double := Map[string](N.Mul(2))
|
||||
|
||||
// Compose: filter then double
|
||||
result1 := double(filter(Right[string](5)))()
|
||||
|
||||
@@ -47,14 +47,14 @@ import (
|
||||
// Example - Remove duplicate integers:
|
||||
//
|
||||
// seq := From(1, 2, 3, 2, 4, 1, 5)
|
||||
// unique := Uniq(func(x int) int { return x })
|
||||
// unique := Uniq(reader.Ask[int]())
|
||||
// result := unique(seq)
|
||||
// // yields: 1, 2, 3, 4, 5
|
||||
//
|
||||
// Example - Unique by string length:
|
||||
//
|
||||
// seq := From("a", "bb", "c", "dd", "eee")
|
||||
// uniqueByLength := Uniq(func(s string) int { return len(s) })
|
||||
// uniqueByLength := Uniq(S.Size)
|
||||
// result := uniqueByLength(seq)
|
||||
// // yields: "a", "bb", "eee" (first occurrence of each length)
|
||||
//
|
||||
@@ -82,14 +82,14 @@ import (
|
||||
// Example - Empty sequence:
|
||||
//
|
||||
// seq := Empty[int]()
|
||||
// unique := Uniq(func(x int) int { return x })
|
||||
// unique := Uniq(reader.Ask[int]())
|
||||
// result := unique(seq)
|
||||
// // yields: nothing (empty sequence)
|
||||
//
|
||||
// Example - All duplicates:
|
||||
//
|
||||
// seq := From(1, 1, 1, 1)
|
||||
// unique := Uniq(func(x int) int { return x })
|
||||
// unique := Uniq(reader.Ask[int]())
|
||||
// result := unique(seq)
|
||||
// // yields: 1 (only first occurrence)
|
||||
func Uniq[A any, K comparable](f func(A) K) Operator[A, A] {
|
||||
|
||||
@@ -377,7 +377,7 @@ func ExampleUniq() {
|
||||
|
||||
func ExampleUniq_byLength() {
|
||||
seq := From("a", "bb", "c", "dd", "eee")
|
||||
uniqueByLength := Uniq(func(s string) int { return len(s) })
|
||||
uniqueByLength := Uniq(S.Size)
|
||||
result := uniqueByLength(seq)
|
||||
|
||||
for v := range result {
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -497,7 +498,7 @@ func TestMapComposition(t *testing.T) {
|
||||
Of(5),
|
||||
Map(N.Mul(2)),
|
||||
Map(N.Add(10)),
|
||||
Map(func(x int) int { return x }),
|
||||
Map(reader.Ask[int]()),
|
||||
)
|
||||
|
||||
assert.Equal(t, 20, result())
|
||||
|
||||
@@ -154,7 +154,7 @@ FunctionMonoid - Creates a monoid for functions when the codomain has a monoid:
|
||||
|
||||
funcMonoid := monoid.FunctionMonoid[string, int](intAddMonoid)
|
||||
|
||||
f1 := func(s string) int { return len(s) }
|
||||
f1 := S.Size
|
||||
f2 := func(s string) int { return len(s) * 2 }
|
||||
|
||||
// Combine functions: result(x) = f1(x) + f2(x)
|
||||
|
||||
@@ -49,7 +49,7 @@ import (
|
||||
// funcMonoid := FunctionMonoid[string, int](intAddMonoid)
|
||||
//
|
||||
// // Define some functions
|
||||
// f1 := func(s string) int { return len(s) }
|
||||
// f1 := S.Size
|
||||
// f2 := func(s string) int { return len(s) * 2 }
|
||||
//
|
||||
// // Combine functions: result(x) = f1(x) + f2(x)
|
||||
|
||||
@@ -262,7 +262,7 @@ func imap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](sa Prism[S, A], ab AB,
|
||||
//
|
||||
// intPrism := MakePrism(...) // Prism[Result, int]
|
||||
// stringPrism := IMap[Result](
|
||||
// func(n int) string { return strconv.Itoa(n) },
|
||||
// strconv.Itoa,
|
||||
// func(s string) int { n, _ := strconv.Atoi(s); return n },
|
||||
// )(intPrism) // Prism[Result, string]
|
||||
func IMap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](ab AB, ba BA) Operator[S, A, B] {
|
||||
|
||||
@@ -156,6 +156,8 @@ func Reverse[T any](o Ord[T]) Ord[T] {
|
||||
// This allows ordering values of type B by first transforming them to type A
|
||||
// and then using the ordering for type A.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A transformation function from B to A
|
||||
//
|
||||
|
||||
@@ -73,7 +73,7 @@ Map operations transform one or both values:
|
||||
// Map both values
|
||||
p4 := pair.MonadBiMap(p,
|
||||
func(n int) string { return fmt.Sprintf("%d", n) },
|
||||
func(s string) int { return len(s) },
|
||||
S.Size,
|
||||
) // Pair[string, int]{"5", 5}
|
||||
|
||||
Curried versions for composition:
|
||||
@@ -91,7 +91,7 @@ Curried versions for composition:
|
||||
// Compose multiple transformations
|
||||
transform := F.Flow2(
|
||||
pair.MapHead[string](N.Mul(2)),
|
||||
pair.MapTail[int](func(s string) int { return len(s) }),
|
||||
pair.MapTail[int](S.Size),
|
||||
)
|
||||
result := transform(p) // Pair[int, int]{10, 5}
|
||||
|
||||
@@ -147,7 +147,7 @@ Apply functions wrapped in pairs to values in pairs:
|
||||
intSum := N.SemigroupSum[int]()
|
||||
|
||||
// Function in a pair
|
||||
pf := pair.MakePair(10, func(s string) int { return len(s) })
|
||||
pf := pair.MakePair(10, S.Size)
|
||||
|
||||
// Value in a pair
|
||||
pv := pair.MakePair(5, "hello")
|
||||
@@ -244,7 +244,7 @@ Functor - Map over values:
|
||||
|
||||
// Functor for tail
|
||||
functor := pair.FunctorTail[int, string, int]()
|
||||
mapper := functor.Map(func(s string) int { return len(s) })
|
||||
mapper := functor.Map(S.Size)
|
||||
|
||||
p := pair.MakePair(5, "hello")
|
||||
result := mapper(p) // Pair[int, int]{5, 5}
|
||||
@@ -267,7 +267,7 @@ Applicative - Apply wrapped functions:
|
||||
applicative := pair.ApplicativeTail[string, int, int](intSum)
|
||||
|
||||
// Create a pair with a function
|
||||
pf := applicative.Of(func(s string) int { return len(s) })
|
||||
pf := applicative.Of(S.Size)
|
||||
|
||||
// Apply to a value
|
||||
pv := pair.MakePair(5, "hello")
|
||||
|
||||
@@ -233,7 +233,7 @@ func PointedTail[B, A any](m monoid.Monoid[A]) pointed.Pointed[B, Pair[A, B]] {
|
||||
// Example:
|
||||
//
|
||||
// functor := pair.FunctorTail[string, int, int]()
|
||||
// mapper := functor.Map(func(s string) int { return len(s) })
|
||||
// mapper := functor.Map(S.Size)
|
||||
// p := pair.MakePair(5, "hello")
|
||||
// p2 := mapper(p) // Pair[int, int]{5, 5}
|
||||
func FunctorTail[B, A, B1 any]() functor.Functor[B, B1, Pair[A, B], Pair[A, B1]] {
|
||||
@@ -250,7 +250,7 @@ func FunctorTail[B, A, B1 any]() functor.Functor[B, B1, Pair[A, B], Pair[A, B1]]
|
||||
//
|
||||
// intSum := M.MonoidSum[int]()
|
||||
// applicative := pair.ApplicativeTail[string, int, int](intSum)
|
||||
// pf := applicative.Of(func(s string) int { return len(s) })
|
||||
// pf := applicative.Of(S.Size)
|
||||
// pv := pair.MakePair(5, "hello")
|
||||
// result := applicative.Ap(pv)(pf) // Pair[int, int]{5, 5}
|
||||
func ApplicativeTail[B, A, B1 any](m monoid.Monoid[A]) applicative.Applicative[B, B1, Pair[A, B], Pair[A, B1], Pair[A, func(B) B1]] {
|
||||
@@ -291,7 +291,7 @@ func Pointed[B, A any](m monoid.Monoid[A]) pointed.Pointed[B, Pair[A, B]] {
|
||||
// Example:
|
||||
//
|
||||
// functor := pair.Functor[string, int, int]()
|
||||
// mapper := functor.Map(func(s string) int { return len(s) })
|
||||
// mapper := functor.Map(S.Size)
|
||||
// p := pair.MakePair(5, "hello")
|
||||
// p2 := mapper(p) // Pair[int, int]{5, 5}
|
||||
func Functor[B, A, B1 any]() functor.Functor[B, B1, Pair[A, B], Pair[A, B1]] {
|
||||
@@ -307,7 +307,7 @@ func Functor[B, A, B1 any]() functor.Functor[B, B1, Pair[A, B], Pair[A, B1]] {
|
||||
//
|
||||
// intSum := M.MonoidSum[int]()
|
||||
// applicative := pair.Applicative[string, int, int](intSum)
|
||||
// pf := applicative.Of(func(s string) int { return len(s) })
|
||||
// pf := applicative.Of(S.Size)
|
||||
// pv := pair.MakePair(5, "hello")
|
||||
// result := applicative.Ap(pv)(pf) // Pair[int, int]{5, 5}
|
||||
func Applicative[B, A, B1 any](m monoid.Monoid[A]) applicative.Applicative[B, B1, Pair[A, B], Pair[A, B1], Pair[A, func(B) B1]] {
|
||||
|
||||
@@ -20,19 +20,91 @@ import (
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
)
|
||||
|
||||
// Monoid creates a simple component-wise monoid for [Pair].
|
||||
//
|
||||
// This function creates a monoid that combines pairs by independently combining their
|
||||
// head and tail components using the provided monoids. Both components are combined
|
||||
// in NORMAL left-to-right order.
|
||||
//
|
||||
// IMPORTANT: This is DIFFERENT from [ApplicativeMonoidTail] and [ApplicativeMonoidHead],
|
||||
// which use applicative functor operations and reverse the order of the non-focused component.
|
||||
//
|
||||
// Use this function when you want:
|
||||
// - Simple, predictable left-to-right combination for both components
|
||||
// - Behavior that matches intuition for non-commutative operations
|
||||
// - Direct component-wise combination without applicative functor semantics
|
||||
//
|
||||
// Use [ApplicativeMonoidTail] or [ApplicativeMonoidHead] when you need applicative
|
||||
// functor semantics for lifting monoid operations into the Pair context.
|
||||
//
|
||||
// Parameters:
|
||||
// - l: A monoid for the head (left) values of type L
|
||||
// - r: A monoid for the tail (right) values of type R
|
||||
//
|
||||
// Returns:
|
||||
// - A Monoid[Pair[L, R]] that combines both components left-to-right
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// S "github.com/IBM/fp-go/v2/string"
|
||||
// )
|
||||
//
|
||||
// intAdd := N.MonoidSum[int]()
|
||||
// strConcat := S.Monoid
|
||||
//
|
||||
// pairMonoid := pair.Monoid(intAdd, strConcat)
|
||||
//
|
||||
// p1 := pair.MakePair(5, "hello")
|
||||
// p2 := pair.MakePair(10, " world")
|
||||
//
|
||||
// result := pairMonoid.Concat(p1, p2)
|
||||
// // result is Pair[int, string]{15, "hello world"}
|
||||
// // Both components combine left-to-right: (5+10, "hello"+" world")
|
||||
//
|
||||
// empty := pairMonoid.Empty()
|
||||
// // empty is Pair[int, string]{0, ""}
|
||||
//
|
||||
// Comparison with ApplicativeMonoidTail:
|
||||
//
|
||||
// strConcat := S.Monoid
|
||||
//
|
||||
// // Simple component-wise monoid
|
||||
// simpleMonoid := pair.Monoid(strConcat, strConcat)
|
||||
// p1 := pair.MakePair("A", "1")
|
||||
// p2 := pair.MakePair("B", "2")
|
||||
// result1 := simpleMonoid.Concat(p1, p2)
|
||||
// // result1 is Pair[string, string]{"AB", "12"}
|
||||
// // Both components: left-to-right
|
||||
//
|
||||
// // Applicative monoid
|
||||
// appMonoid := pair.ApplicativeMonoidTail(strConcat, strConcat)
|
||||
// result2 := appMonoid.Concat(p1, p2)
|
||||
// // result2 is Pair[string, string]{"BA", "12"}
|
||||
// // Head: reversed, Tail: normal
|
||||
//
|
||||
//go:inline
|
||||
func Monoid[L, R any](l M.Monoid[L], r M.Monoid[R]) M.Monoid[Pair[L, R]] {
|
||||
return M.MakeMonoid(
|
||||
func(pl, pr Pair[L, R]) Pair[L, R] {
|
||||
return MakePair(l.Concat(Head(pl), Head(pr)), r.Concat(Tail(pl), Tail(pr)))
|
||||
},
|
||||
MakePair(l.Empty(), r.Empty()),
|
||||
)
|
||||
}
|
||||
|
||||
// ApplicativeMonoid creates a monoid for [Pair] using applicative functor operations on the tail.
|
||||
//
|
||||
// This is an alias for [ApplicativeMonoidTail], which lifts the right (tail) monoid into the
|
||||
// Pair applicative functor. The left monoid provides the semigroup for combining head values
|
||||
// during applicative operations.
|
||||
//
|
||||
// IMPORTANT: The three monoid constructors (ApplicativeMonoid/ApplicativeMonoidTail and
|
||||
// ApplicativeMonoidHead) produce DIFFERENT results:
|
||||
// - ApplicativeMonoidTail: Combines head values in REVERSE order (right-to-left)
|
||||
// - ApplicativeMonoidHead: Combines tail values in REVERSE order (right-to-left)
|
||||
// - The "focused" component (tail for Tail, head for Head) combines in normal order (left-to-right)
|
||||
//
|
||||
// This difference is significant for non-commutative operations like string concatenation.
|
||||
// IMPORTANT BEHAVIORAL NOTE: The applicative implementation causes the HEAD component to be
|
||||
// combined in REVERSE order (right-to-left) while the TAIL combines normally (left-to-right).
|
||||
// This differs from Haskell's standard Applicative instance for pairs, which combines the
|
||||
// first component left-to-right. This matters for non-commutative operations like string
|
||||
// concatenation.
|
||||
//
|
||||
// Parameters:
|
||||
// - l: A monoid for the head (left) values of type L
|
||||
@@ -74,9 +146,16 @@ func ApplicativeMonoid[L, R any](l M.Monoid[L], r M.Monoid[R]) M.Monoid[Pair[L,
|
||||
// the tail (right) value. The head values are combined using the left monoid's semigroup
|
||||
// operation during applicative application.
|
||||
//
|
||||
// CRITICAL BEHAVIOR: Due to the applicative functor implementation, the HEAD values are
|
||||
// combined in REVERSE order (right-to-left), while TAIL values combine in normal order
|
||||
// (left-to-right). This matters for non-commutative operations:
|
||||
// CRITICAL BEHAVIORAL NOTE: The HEAD values are combined in REVERSE order (right-to-left),
|
||||
// while TAIL values combine in normal order (left-to-right). This is due to how the
|
||||
// applicative `ap` operation is implemented for Pair.
|
||||
//
|
||||
// NOTE: This differs from Haskell's standard Applicative instance for (,) which combines
|
||||
// the first component left-to-right. The reversal occurs because MonadApTail implements:
|
||||
//
|
||||
// MakePair(sg.Concat(second.head, first.head), ...)
|
||||
//
|
||||
// Example showing the reversal with non-commutative operations:
|
||||
//
|
||||
// strConcat := S.Monoid
|
||||
// pairMonoid := pair.ApplicativeMonoidTail(strConcat, strConcat)
|
||||
@@ -85,7 +164,9 @@ func ApplicativeMonoid[L, R any](l M.Monoid[L], r M.Monoid[R]) M.Monoid[Pair[L,
|
||||
// result := pairMonoid.Concat(p1, p2)
|
||||
// // result is Pair[string, string]{" worldhello", "foobar"}
|
||||
// // ^^^^^^^^^^^^^^ ^^^^^^
|
||||
// // REVERSED! normal
|
||||
// // REVERSED! normal order
|
||||
//
|
||||
// In Haskell's Applicative for (,), this would give ("hellohello world", "foobar")
|
||||
//
|
||||
// The resulting monoid satisfies the standard monoid laws:
|
||||
// - Associativity: Concat(Concat(p1, p2), p3) = Concat(p1, Concat(p2, p3))
|
||||
@@ -154,9 +235,13 @@ func ApplicativeMonoidTail[L, R any](l M.Monoid[L], r M.Monoid[R]) M.Monoid[Pair
|
||||
//
|
||||
// This is the dual of [ApplicativeMonoidTail], operating on the head instead of the tail.
|
||||
//
|
||||
// CRITICAL BEHAVIOR: Due to the applicative functor implementation, the TAIL values are
|
||||
// combined in REVERSE order (right-to-left), while HEAD values combine in normal order
|
||||
// (left-to-right). This is the opposite of ApplicativeMonoidTail:
|
||||
// CRITICAL BEHAVIORAL NOTE: The TAIL values are combined in REVERSE order (right-to-left),
|
||||
// while HEAD values combine in normal order (left-to-right). This is the opposite behavior
|
||||
// of ApplicativeMonoidTail. The reversal occurs because MonadApHead implements:
|
||||
//
|
||||
// MakePair(..., sg.Concat(second.tail, first.tail))
|
||||
//
|
||||
// Example showing the reversal with non-commutative operations:
|
||||
//
|
||||
// strConcat := S.Monoid
|
||||
// pairMonoid := pair.ApplicativeMonoidHead(strConcat, strConcat)
|
||||
@@ -165,7 +250,7 @@ func ApplicativeMonoidTail[L, R any](l M.Monoid[L], r M.Monoid[R]) M.Monoid[Pair
|
||||
// result := pairMonoid.Concat(p1, p2)
|
||||
// // result is Pair[string, string]{"hello world", "barfoo"}
|
||||
// // ^^^^^^^^^^^^ ^^^^^^^^
|
||||
// // normal REVERSED!
|
||||
// // normal order REVERSED!
|
||||
//
|
||||
// The resulting monoid satisfies the standard monoid laws:
|
||||
// - Associativity: Concat(Concat(p1, p2), p3) = Concat(p1, Concat(p2, p3))
|
||||
|
||||
@@ -495,3 +495,262 @@ func TestMonoidCommutativity(t *testing.T) {
|
||||
assert.NotEqual(t, result1, result2)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSimpleMonoid tests the basic Monoid function (non-applicative)
|
||||
func TestSimpleMonoid(t *testing.T) {
|
||||
t.Run("combines both components left-to-right", func(t *testing.T) {
|
||||
strConcat := S.Monoid
|
||||
|
||||
pairMonoid := Monoid(strConcat, strConcat)
|
||||
|
||||
p1 := MakePair("hello", "foo")
|
||||
p2 := MakePair(" world", "bar")
|
||||
|
||||
result := pairMonoid.Concat(p1, p2)
|
||||
|
||||
// Both components combine in normal left-to-right order
|
||||
assert.Equal(t, "hello world", Head(result))
|
||||
assert.Equal(t, "foobar", Tail(result))
|
||||
})
|
||||
|
||||
t.Run("empty value", func(t *testing.T) {
|
||||
intAdd := N.MonoidSum[int]()
|
||||
strConcat := S.Monoid
|
||||
|
||||
pairMonoid := Monoid(intAdd, strConcat)
|
||||
|
||||
empty := pairMonoid.Empty()
|
||||
assert.Equal(t, 0, Head(empty))
|
||||
assert.Equal(t, "", Tail(empty))
|
||||
})
|
||||
|
||||
t.Run("monoid laws", func(t *testing.T) {
|
||||
intAdd := N.MonoidSum[int]()
|
||||
intMul := N.MonoidProduct[int]()
|
||||
|
||||
pairMonoid := Monoid(intAdd, intMul)
|
||||
|
||||
p1 := MakePair(1, 2)
|
||||
p2 := MakePair(3, 4)
|
||||
p3 := MakePair(5, 6)
|
||||
|
||||
// Associativity
|
||||
left := pairMonoid.Concat(pairMonoid.Concat(p1, p2), p3)
|
||||
right := pairMonoid.Concat(p1, pairMonoid.Concat(p2, p3))
|
||||
assert.Equal(t, left, right)
|
||||
|
||||
// Left identity
|
||||
assert.Equal(t, p1, pairMonoid.Concat(pairMonoid.Empty(), p1))
|
||||
|
||||
// Right identity
|
||||
assert.Equal(t, p1, pairMonoid.Concat(p1, pairMonoid.Empty()))
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonoidComparison compares the simple Monoid with applicative versions
|
||||
func TestMonoidComparison(t *testing.T) {
|
||||
t.Run("Monoid vs ApplicativeMonoidTail with strings", func(t *testing.T) {
|
||||
strConcat := S.Monoid
|
||||
|
||||
simpleMonoid := Monoid(strConcat, strConcat)
|
||||
appMonoid := ApplicativeMonoidTail(strConcat, strConcat)
|
||||
|
||||
p1 := MakePair("A", "1")
|
||||
p2 := MakePair("B", "2")
|
||||
|
||||
simpleResult := simpleMonoid.Concat(p1, p2)
|
||||
appResult := appMonoid.Concat(p1, p2)
|
||||
|
||||
// Simple monoid: both components left-to-right
|
||||
assert.Equal(t, "AB", Head(simpleResult))
|
||||
assert.Equal(t, "12", Tail(simpleResult))
|
||||
|
||||
// Applicative monoid: head reversed, tail normal
|
||||
assert.Equal(t, "BA", Head(appResult))
|
||||
assert.Equal(t, "12", Tail(appResult))
|
||||
|
||||
// They produce different results!
|
||||
assert.NotEqual(t, simpleResult, appResult)
|
||||
})
|
||||
|
||||
t.Run("Monoid vs ApplicativeMonoidHead with strings", func(t *testing.T) {
|
||||
strConcat := S.Monoid
|
||||
|
||||
simpleMonoid := Monoid(strConcat, strConcat)
|
||||
appMonoid := ApplicativeMonoidHead(strConcat, strConcat)
|
||||
|
||||
p1 := MakePair("A", "1")
|
||||
p2 := MakePair("B", "2")
|
||||
|
||||
simpleResult := simpleMonoid.Concat(p1, p2)
|
||||
appResult := appMonoid.Concat(p1, p2)
|
||||
|
||||
// Simple monoid: both components left-to-right
|
||||
assert.Equal(t, "AB", Head(simpleResult))
|
||||
assert.Equal(t, "12", Tail(simpleResult))
|
||||
|
||||
// Applicative monoid: head normal, tail reversed
|
||||
assert.Equal(t, "AB", Head(appResult))
|
||||
assert.Equal(t, "21", Tail(appResult))
|
||||
|
||||
// They produce different results!
|
||||
assert.NotEqual(t, simpleResult, appResult)
|
||||
})
|
||||
|
||||
t.Run("all three produce same result with commutative operations", func(t *testing.T) {
|
||||
intAdd := N.MonoidSum[int]()
|
||||
intMul := N.MonoidProduct[int]()
|
||||
|
||||
simpleMonoid := Monoid(intAdd, intMul)
|
||||
appTailMonoid := ApplicativeMonoidTail(intAdd, intMul)
|
||||
appHeadMonoid := ApplicativeMonoidHead(intAdd, intMul)
|
||||
|
||||
p1 := MakePair(2, 3)
|
||||
p2 := MakePair(4, 5)
|
||||
|
||||
simpleResult := simpleMonoid.Concat(p1, p2)
|
||||
appTailResult := appTailMonoid.Concat(p1, p2)
|
||||
appHeadResult := appHeadMonoid.Concat(p1, p2)
|
||||
|
||||
// All produce the same result with commutative operations
|
||||
// Simple: (2+4, 3*5) = (6, 15)
|
||||
// AppTail: (4+2, 3*5) = (6, 15) - addition is commutative
|
||||
// AppHead: (2+4, 5*3) = (6, 15) - multiplication is commutative
|
||||
assert.Equal(t, 6, Head(simpleResult))
|
||||
assert.Equal(t, 15, Tail(simpleResult))
|
||||
assert.Equal(t, simpleResult, appTailResult)
|
||||
assert.Equal(t, simpleResult, appHeadResult)
|
||||
})
|
||||
|
||||
t.Run("Monoid matches Haskell behavior", func(t *testing.T) {
|
||||
strConcat := S.Monoid
|
||||
|
||||
pairMonoid := Monoid(strConcat, strConcat)
|
||||
|
||||
p1 := MakePair("hello", "foo")
|
||||
p2 := MakePair(" world", "bar")
|
||||
|
||||
result := pairMonoid.Concat(p1, p2)
|
||||
|
||||
// This matches what you'd expect from simple tuple combination
|
||||
// and is closer to intuitive behavior
|
||||
assert.Equal(t, "hello world", Head(result))
|
||||
assert.Equal(t, "foobar", Tail(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonoidHaskellComparison documents how this implementation differs from Haskell's
|
||||
// standard Applicative instance for pairs (tuples).
|
||||
func TestMonoidHaskellComparison(t *testing.T) {
|
||||
t.Run("ApplicativeMonoidTail reverses head order unlike Haskell", func(t *testing.T) {
|
||||
strConcat := S.Monoid
|
||||
|
||||
pairMonoid := ApplicativeMonoidTail(strConcat, strConcat)
|
||||
|
||||
p1 := MakePair("hello", "foo")
|
||||
p2 := MakePair(" world", "bar")
|
||||
|
||||
result := pairMonoid.Concat(p1, p2)
|
||||
|
||||
// Go implementation: head is reversed, tail is normal
|
||||
assert.Equal(t, " worldhello", Head(result))
|
||||
assert.Equal(t, "foobar", Tail(result))
|
||||
|
||||
// In Haskell's Applicative for (,):
|
||||
// (u, f) <*> (v, x) = (u <> v, f x)
|
||||
// pure (<>) <*> ("hello", "foo") <*> (" world", "bar")
|
||||
// would give: ("hello world", "foobar")
|
||||
// Note: Haskell combines first component left-to-right, not reversed
|
||||
})
|
||||
|
||||
t.Run("ApplicativeMonoidHead reverses tail order", func(t *testing.T) {
|
||||
strConcat := S.Monoid
|
||||
|
||||
pairMonoid := ApplicativeMonoidHead(strConcat, strConcat)
|
||||
|
||||
p1 := MakePair("hello", "foo")
|
||||
p2 := MakePair(" world", "bar")
|
||||
|
||||
result := pairMonoid.Concat(p1, p2)
|
||||
|
||||
// Go implementation: head is normal, tail is reversed
|
||||
assert.Equal(t, "hello world", Head(result))
|
||||
assert.Equal(t, "barfoo", Tail(result))
|
||||
|
||||
// This is the dual operation, focusing on head instead of tail
|
||||
})
|
||||
|
||||
t.Run("behavior with commutative operations matches Haskell", func(t *testing.T) {
|
||||
intAdd := N.MonoidSum[int]()
|
||||
intMul := N.MonoidProduct[int]()
|
||||
|
||||
pairMonoid := ApplicativeMonoidTail(intAdd, intMul)
|
||||
|
||||
p1 := MakePair(5, 3)
|
||||
p2 := MakePair(10, 4)
|
||||
|
||||
result := pairMonoid.Concat(p1, p2)
|
||||
|
||||
// With commutative operations, order doesn't matter
|
||||
// Both Go and Haskell give the same result
|
||||
assert.Equal(t, 15, Head(result)) // 5 + 10 = 10 + 5
|
||||
assert.Equal(t, 12, Tail(result)) // 3 * 4 = 4 * 3
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonoidOrderingDocumentation provides clear examples of the ordering behavior
|
||||
// for documentation purposes.
|
||||
func TestMonoidOrderingDocumentation(t *testing.T) {
|
||||
t.Run("ApplicativeMonoidTail ordering", func(t *testing.T) {
|
||||
strConcat := S.Monoid
|
||||
pairMonoid := ApplicativeMonoidTail(strConcat, strConcat)
|
||||
|
||||
p1 := MakePair("A", "1")
|
||||
p2 := MakePair("B", "2")
|
||||
p3 := MakePair("C", "3")
|
||||
|
||||
// Concat p1 and p2
|
||||
r12 := pairMonoid.Concat(p1, p2)
|
||||
assert.Equal(t, "BA", Head(r12)) // Head: reversed (p2 + p1)
|
||||
assert.Equal(t, "12", Tail(r12)) // Tail: normal (p1 + p2)
|
||||
|
||||
// Concat all three
|
||||
r123 := pairMonoid.Concat(r12, p3)
|
||||
assert.Equal(t, "CBA", Head(r123)) // Head: reversed (p3 + p2 + p1)
|
||||
assert.Equal(t, "123", Tail(r123)) // Tail: normal (p1 + p2 + p3)
|
||||
})
|
||||
|
||||
t.Run("ApplicativeMonoidHead ordering", func(t *testing.T) {
|
||||
strConcat := S.Monoid
|
||||
pairMonoid := ApplicativeMonoidHead(strConcat, strConcat)
|
||||
|
||||
p1 := MakePair("A", "1")
|
||||
p2 := MakePair("B", "2")
|
||||
p3 := MakePair("C", "3")
|
||||
|
||||
// Concat p1 and p2
|
||||
r12 := pairMonoid.Concat(p1, p2)
|
||||
assert.Equal(t, "AB", Head(r12)) // Head: normal (p1 + p2)
|
||||
assert.Equal(t, "21", Tail(r12)) // Tail: reversed (p2 + p1)
|
||||
|
||||
// Concat all three
|
||||
r123 := pairMonoid.Concat(r12, p3)
|
||||
assert.Equal(t, "ABC", Head(r123)) // Head: normal (p1 + p2 + p3)
|
||||
assert.Equal(t, "321", Tail(r123)) // Tail: reversed (p3 + p2 + p1)
|
||||
})
|
||||
|
||||
t.Run("empty values respect ordering", func(t *testing.T) {
|
||||
strConcat := S.Monoid
|
||||
pairMonoid := ApplicativeMonoidTail(strConcat, strConcat)
|
||||
|
||||
empty := pairMonoid.Empty()
|
||||
p := MakePair("X", "Y")
|
||||
|
||||
// Empty is identity regardless of order
|
||||
r1 := pairMonoid.Concat(empty, p)
|
||||
r2 := pairMonoid.Concat(p, empty)
|
||||
|
||||
assert.Equal(t, p, r1)
|
||||
assert.Equal(t, p, r2)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ func MonadMapTail[A, B, B1 any](fa Pair[A, B], f func(B) B1) Pair[A, B1] {
|
||||
// p := pair.MakePair(5, "hello")
|
||||
// p2 := pair.MonadBiMap(p,
|
||||
// func(n int) string { return fmt.Sprintf("%d", n) },
|
||||
// func(s string) int { return len(s) },
|
||||
// S.Size,
|
||||
// ) // Pair[string, int]{"5", 5}
|
||||
//
|
||||
//go:inline
|
||||
@@ -202,7 +202,7 @@ func MonadBiMap[A, B, A1, B1 any](fa Pair[A, B], f func(A) A1, g func(B) B1) Pai
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// mapper := pair.Map[int](func(s string) int { return len(s) })
|
||||
// mapper := pair.Map[int](S.Size)
|
||||
// p := pair.MakePair(5, "hello")
|
||||
// p2 := mapper(p) // Pair[int, int]{5, 5}
|
||||
//
|
||||
@@ -232,7 +232,7 @@ func MapHead[B, A, A1 any](f func(A) A1) func(Pair[A, B]) Pair[A1, B] {
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// mapper := pair.MapTail[int](func(s string) int { return len(s) })
|
||||
// mapper := pair.MapTail[int](S.Size)
|
||||
// p := pair.MakePair(5, "hello")
|
||||
// p2 := mapper(p) // Pair[int, int]{5, 5}
|
||||
//
|
||||
@@ -248,7 +248,7 @@ func MapTail[A, B, B1 any](f func(B) B1) Operator[A, B, B1] {
|
||||
//
|
||||
// mapper := pair.BiMap(
|
||||
// func(n int) string { return fmt.Sprintf("%d", n) },
|
||||
// func(s string) int { return len(s) },
|
||||
// S.Size,
|
||||
// )
|
||||
// p := pair.MakePair(5, "hello")
|
||||
// p2 := mapper(p) // Pair[string, int]{"5", 5}
|
||||
@@ -400,7 +400,7 @@ func MonadApHead[B, A, A1 any](sg Semigroup[B], faa Pair[func(A) A1, B], fa Pair
|
||||
// import N "github.com/IBM/fp-go/v2/number"
|
||||
//
|
||||
// intSum := N.SemigroupSum[int]()
|
||||
// pf := pair.MakePair(10, func(s string) int { return len(s) })
|
||||
// pf := pair.MakePair(10, S.Size)
|
||||
// pv := pair.MakePair(5, "hello")
|
||||
// result := pair.MonadApTail(intSum, pf, pv) // Pair[int, int]{15, 5}
|
||||
//
|
||||
@@ -417,7 +417,7 @@ func MonadApTail[A, B, B1 any](sg Semigroup[A], fbb Pair[A, func(B) B1], fb Pair
|
||||
// import N "github.com/IBM/fp-go/v2/number"
|
||||
//
|
||||
// intSum := N.SemigroupSum[int]()
|
||||
// pf := pair.MakePair(10, func(s string) int { return len(s) })
|
||||
// pf := pair.MakePair(10, S.Size)
|
||||
// pv := pair.MakePair(5, "hello")
|
||||
// result := pair.MonadAp(intSum, pf, pv) // Pair[int, int]{15, 5}
|
||||
//
|
||||
@@ -454,7 +454,7 @@ func ApHead[B, A, A1 any](sg Semigroup[B], fa Pair[A, B]) func(Pair[func(A) A1,
|
||||
// intSum := N.SemigroupSum[int]()
|
||||
// pv := pair.MakePair(5, "hello")
|
||||
// ap := pair.ApTail(intSum, pv)
|
||||
// pf := pair.MakePair(10, func(s string) int { return len(s) })
|
||||
// pf := pair.MakePair(10, S.Size)
|
||||
// result := ap(pf) // Pair[int, int]{15, 5}
|
||||
func ApTail[A, B, B1 any](sg Semigroup[A], fb Pair[A, B]) Operator[A, func(B) B1, B1] {
|
||||
return func(fbb Pair[A, func(B) B1]) Pair[A, B1] {
|
||||
@@ -472,7 +472,7 @@ func ApTail[A, B, B1 any](sg Semigroup[A], fb Pair[A, B]) Operator[A, func(B) B1
|
||||
// intSum := N.SemigroupSum[int]()
|
||||
// pv := pair.MakePair(5, "hello")
|
||||
// ap := pair.Ap(intSum, pv)
|
||||
// pf := pair.MakePair(10, func(s string) int { return len(s) })
|
||||
// pf := pair.MakePair(10, S.Size)
|
||||
// result := ap(pf) // Pair[int, int]{15, 5}
|
||||
//
|
||||
//go:inline
|
||||
|
||||
78
v2/reader/profunctor.go
Normal file
78
v2/reader/profunctor.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package reader
|
||||
|
||||
import "github.com/IBM/fp-go/v2/function"
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a Reader.
|
||||
// It applies f to the input (contravariantly) and g to the output (covariantly).
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Port int }
|
||||
// type Env struct { Config Config }
|
||||
// getPort := func(c Config) int { return c.Port }
|
||||
// extractConfig := func(e Env) Config { return e.Config }
|
||||
// toString := strconv.Itoa
|
||||
// r := reader.Promap(extractConfig, toString)(getPort)
|
||||
// result := r(Env{Config: Config{Port: 8080}}) // "8080"
|
||||
func Promap[E, A, D, B any](f func(D) E, g func(A) B) Kleisli[D, Reader[E, A], B] {
|
||||
return function.Bind13of3(function.Flow3[func(D) E, func(E) A, func(A) B])(f, g)
|
||||
}
|
||||
|
||||
// Local changes the value of the local context during the execution of the action `ma`.
|
||||
// This is similar to Contravariant's contramap and allows you to modify the environment
|
||||
// before passing it to a Reader.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type DetailedConfig struct { Host string; Port int }
|
||||
// type SimpleConfig struct { Host string }
|
||||
// getHost := func(c SimpleConfig) string { return c.Host }
|
||||
// simplify := func(d DetailedConfig) SimpleConfig { return SimpleConfig{Host: d.Host} }
|
||||
// r := reader.Local(simplify)(getHost)
|
||||
// result := r(DetailedConfig{Host: "localhost", Port: 8080}) // "localhost"
|
||||
//
|
||||
//go:inline
|
||||
func Local[A, R1, R2 any](f func(R2) R1) Kleisli[R2, Reader[R1, A], A] {
|
||||
return Compose[A](f)
|
||||
}
|
||||
|
||||
// Contramap is an alias for Local.
|
||||
// It changes the value of the local context during the execution of a Reader.
|
||||
// This is the contravariant functor operation that transforms the input environment.
|
||||
//
|
||||
// Contramap is semantically identical to Local - both modify the environment before
|
||||
// passing it to a Reader. The name "Contramap" emphasizes the contravariant nature
|
||||
// of the transformation (transforming the input rather than the output).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type DetailedConfig struct { Host string; Port int }
|
||||
// type SimpleConfig struct { Host string }
|
||||
// getHost := func(c SimpleConfig) string { return c.Host }
|
||||
// simplify := func(d DetailedConfig) SimpleConfig { return SimpleConfig{Host: d.Host} }
|
||||
// r := reader.Contramap(simplify)(getHost)
|
||||
// result := r(DetailedConfig{Host: "localhost", Port: 8080}) // "localhost"
|
||||
//
|
||||
// See also: Local
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[A, R1, R2 any](f func(R2) R1) Kleisli[R2, Reader[R1, A], A] {
|
||||
return Compose[A](f)
|
||||
}
|
||||
@@ -367,10 +367,10 @@ func Flatten[R, A any](mma Reader[R, Reader[R, A]]) Reader[R, A] {
|
||||
// getConfig := func(e Env) Config { return e.Config }
|
||||
// getPort := func(c Config) int { return c.Port }
|
||||
// getPortFromEnv := reader.Compose(getConfig)(getPort)
|
||||
//
|
||||
//go:inline
|
||||
func Compose[C, R, B any](ab Reader[R, B]) Kleisli[R, Reader[B, C], C] {
|
||||
return func(bc Reader[B, C]) Reader[R, C] {
|
||||
return function.Flow2(ab, bc)
|
||||
}
|
||||
return function.Bind1st(function.Flow2[Reader[R, B], Reader[B, C]], ab)
|
||||
}
|
||||
|
||||
// First applies a Reader to the first element of a tuple, leaving the second element unchanged.
|
||||
@@ -401,42 +401,6 @@ func Second[A, B, C any](pbc Reader[B, C]) Reader[T.Tuple2[A, B], T.Tuple2[A, C]
|
||||
}
|
||||
}
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a Reader.
|
||||
// It applies f to the input (contravariantly) and g to the output (covariantly).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Port int }
|
||||
// type Env struct { Config Config }
|
||||
// getPort := func(c Config) int { return c.Port }
|
||||
// extractConfig := func(e Env) Config { return e.Config }
|
||||
// toString := strconv.Itoa
|
||||
// r := reader.Promap(extractConfig, toString)(getPort)
|
||||
// result := r(Env{Config: Config{Port: 8080}}) // "8080"
|
||||
func Promap[E, A, D, B any](f func(D) E, g func(A) B) Kleisli[D, Reader[E, A], B] {
|
||||
return func(fea Reader[E, A]) Reader[D, B] {
|
||||
return function.Flow3(f, fea, g)
|
||||
}
|
||||
}
|
||||
|
||||
// Local changes the value of the local context during the execution of the action `ma`.
|
||||
// This is similar to Contravariant's contramap and allows you to modify the environment
|
||||
// before passing it to a Reader.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type DetailedConfig struct { Host string; Port int }
|
||||
// type SimpleConfig struct { Host string }
|
||||
// getHost := func(c SimpleConfig) string { return c.Host }
|
||||
// simplify := func(d DetailedConfig) SimpleConfig { return SimpleConfig{Host: d.Host} }
|
||||
// r := reader.Local(simplify)(getHost)
|
||||
// result := r(DetailedConfig{Host: "localhost", Port: 8080}) // "localhost"
|
||||
//
|
||||
//go:inline
|
||||
func Local[A, R2, R1 any](f func(R2) R1) Kleisli[R2, Reader[R1, A], A] {
|
||||
return Compose[A](f)
|
||||
}
|
||||
|
||||
// Read applies a context to a Reader to obtain its value.
|
||||
// This is the "run" operation that executes a Reader with a specific environment.
|
||||
//
|
||||
@@ -446,8 +410,10 @@ func Local[A, R2, R1 any](f func(R2) R1) Kleisli[R2, Reader[R1, A], A] {
|
||||
// getPort := reader.Asks(func(c Config) int { return c.Port })
|
||||
// run := reader.Read(Config{Port: 8080})
|
||||
// port := run(getPort) // 8080
|
||||
//
|
||||
//go:inline
|
||||
func Read[A, E any](e E) func(Reader[E, A]) A {
|
||||
return I.Ap[A](e)
|
||||
return I.Flap[A](e)
|
||||
}
|
||||
|
||||
// MonadFlap is the monadic version of Flap.
|
||||
@@ -461,6 +427,8 @@ func Read[A, E any](e E) func(Reader[E, A]) A {
|
||||
// }
|
||||
// r := reader.MonadFlap(getMultiplier, 5)
|
||||
// result := r(Config{Multiplier: 3}) // 15
|
||||
//
|
||||
//go:inline
|
||||
func MonadFlap[R, B, A any](fab Reader[R, func(A) B], a A) Reader[R, B] {
|
||||
return functor.MonadFlap(MonadMap[R, func(A) B, B], fab, a)
|
||||
}
|
||||
@@ -477,6 +445,8 @@ func MonadFlap[R, B, A any](fab Reader[R, func(A) B], a A) Reader[R, B] {
|
||||
// applyTo5 := reader.Flap[Config](5)
|
||||
// r := applyTo5(getMultiplier)
|
||||
// result := r(Config{Multiplier: 3}) // 15
|
||||
//
|
||||
//go:inline
|
||||
func Flap[R, B, A any](a A) Operator[R, func(A) B, B] {
|
||||
return functor.Flap(Map[R, func(A) B, B], a)
|
||||
}
|
||||
|
||||
76
v2/readereither/profunctor.go
Normal file
76
v2/readereither/profunctor.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readereither
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a ReaderEither.
|
||||
// It applies f to the input environment (contravariantly) and g to the output value (covariantly).
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// This operation allows you to:
|
||||
// - Adapt the environment type before passing it to the ReaderEither (via f)
|
||||
// - Transform the success value after the computation completes (via g)
|
||||
//
|
||||
// The error type E remains unchanged through the transformation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The original environment type expected by the ReaderEither
|
||||
// - E: The error type (unchanged)
|
||||
// - A: The original success type produced by the ReaderEither
|
||||
// - D: The new input environment type
|
||||
// - B: The new output success type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the input environment from D to R (contravariant)
|
||||
// - g: Function to transform the output success value from A to B (covariant)
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderEither[R, E, A] and returns a ReaderEither[D, E, B]
|
||||
//
|
||||
//go:inline
|
||||
func Promap[R, E, A, D, B any](f func(D) R, g func(A) B) Kleisli[D, E, ReaderEither[R, E, A], B] {
|
||||
return reader.Promap(f, either.Map[E](g))
|
||||
}
|
||||
|
||||
// Contramap changes the value of the local environment during the execution of a ReaderEither.
|
||||
// This is the contravariant functor operation that transforms the input environment.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// Contramap is useful for adapting a ReaderEither to work with a different environment type
|
||||
// by providing a function that converts the new environment to the expected one.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The error type (unchanged)
|
||||
// - A: The success type (unchanged)
|
||||
// - R2: The new input environment type
|
||||
// - R1: The original environment type expected by the ReaderEither
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the environment from R2 to R1
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderEither[R1, E, A] and returns a ReaderEither[R2, E, A]
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[E, A, R1, R2 any](f func(R2) R1) Kleisli[R2, E, ReaderEither[R1, E, A], A] {
|
||||
return reader.Contramap[Either[E, A]](f)
|
||||
}
|
||||
135
v2/readereither/profunctor_test.go
Normal file
135
v2/readereither/profunctor_test.go
Normal file
@@ -0,0 +1,135 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readereither
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type SimpleConfig struct {
|
||||
Port int
|
||||
}
|
||||
|
||||
type DetailedConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
// TestPromapBasic tests basic Promap functionality
|
||||
func TestPromapBasic(t *testing.T) {
|
||||
t.Run("transform both input and output", func(t *testing.T) {
|
||||
// ReaderEither that reads port from SimpleConfig
|
||||
getPort := func(c SimpleConfig) Either[string, int] {
|
||||
return E.Of[string](c.Port)
|
||||
}
|
||||
|
||||
// Transform DetailedConfig to SimpleConfig and int to string
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap[SimpleConfig, string](simplify, toString)(getPort)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 8080})
|
||||
|
||||
assert.Equal(t, E.Of[string]("8080"), result)
|
||||
})
|
||||
|
||||
t.Run("handles error case", func(t *testing.T) {
|
||||
// ReaderEither that returns an error
|
||||
getError := func(c SimpleConfig) Either[string, int] {
|
||||
return E.Left[int]("error occurred")
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap[SimpleConfig, string](simplify, toString)(getError)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 8080})
|
||||
|
||||
assert.Equal(t, E.Left[string]("error occurred"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestContramapBasic tests basic Contramap functionality
|
||||
func TestContramapBasic(t *testing.T) {
|
||||
t.Run("environment adaptation", func(t *testing.T) {
|
||||
// ReaderEither that reads from SimpleConfig
|
||||
getPort := func(c SimpleConfig) Either[string, int] {
|
||||
return E.Of[string](c.Port)
|
||||
}
|
||||
|
||||
// Adapt to work with DetailedConfig
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
|
||||
adapted := Contramap[string, int](simplify)(getPort)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 9000})
|
||||
|
||||
assert.Equal(t, E.Of[string](9000), result)
|
||||
})
|
||||
|
||||
t.Run("preserves error", func(t *testing.T) {
|
||||
getError := func(c SimpleConfig) Either[string, int] {
|
||||
return E.Left[int]("config error")
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
|
||||
adapted := Contramap[string, int](simplify)(getError)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 9000})
|
||||
|
||||
assert.Equal(t, E.Left[int]("config error"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapComposition tests that Promap can be composed
|
||||
func TestPromapComposition(t *testing.T) {
|
||||
t.Run("compose two Promap transformations", func(t *testing.T) {
|
||||
type Config1 struct{ Value int }
|
||||
type Config2 struct{ Value int }
|
||||
type Config3 struct{ Value int }
|
||||
|
||||
reader := func(c Config1) Either[string, int] {
|
||||
return E.Of[string](c.Value)
|
||||
}
|
||||
|
||||
f1 := func(c2 Config2) Config1 { return Config1{Value: c2.Value} }
|
||||
g1 := N.Mul(2)
|
||||
|
||||
f2 := func(c3 Config3) Config2 { return Config2{Value: c3.Value} }
|
||||
g2 := N.Add(10)
|
||||
|
||||
// Apply two Promap transformations
|
||||
step1 := Promap[Config1, string](f1, g1)(reader)
|
||||
step2 := Promap[Config2, string](f2, g2)(step1)
|
||||
|
||||
result := step2(Config3{Value: 5})
|
||||
|
||||
// (5 * 2) + 10 = 20
|
||||
assert.Equal(t, E.Of[string](20), result)
|
||||
})
|
||||
}
|
||||
@@ -151,7 +151,7 @@ func BiMap[E, E1, E2, A, B any](f func(E1) E2, g func(A) B) func(ReaderEither[E,
|
||||
|
||||
// Local changes the value of the local context during the execution of the action `ma` (similar to `Contravariant`'s
|
||||
// `contramap`).
|
||||
func Local[E, A, R2, R1 any](f func(R2) R1) func(ReaderEither[R1, E, A]) ReaderEither[R2, E, A] {
|
||||
func Local[E, A, R1, R2 any](f func(R2) R1) func(ReaderEither[R1, E, A]) ReaderEither[R2, E, A] {
|
||||
return reader.Local[Either[E, A]](f)
|
||||
}
|
||||
|
||||
|
||||
236
v2/readerio/profunctor.go
Normal file
236
v2/readerio/profunctor.go
Normal file
@@ -0,0 +1,236 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerio
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a ReaderIO.
|
||||
// It applies f to the input environment (contravariantly) and g to the output value (covariantly).
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// This operation allows you to:
|
||||
// - Adapt the environment type before passing it to the ReaderIO (via f)
|
||||
// - Transform the result value after the IO effect completes (via g)
|
||||
//
|
||||
// The transformation happens in two stages:
|
||||
// 1. The input environment D is transformed to E using f (contravariant)
|
||||
// 2. The ReaderIO[E, A] is executed with the transformed environment
|
||||
// 3. The result value A is transformed to B using g (covariant) within the IO context
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The original environment type expected by the ReaderIO
|
||||
// - A: The original result type produced by the ReaderIO
|
||||
// - D: The new input environment type
|
||||
// - B: The new output result type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the input environment from D to E (contravariant)
|
||||
// - g: Function to transform the output value from A to B (covariant)
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderIO[E, A] and returns a ReaderIO[D, B]
|
||||
//
|
||||
// Example - Adapting environment and transforming result:
|
||||
//
|
||||
// type DetailedConfig struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// Debug bool
|
||||
// }
|
||||
//
|
||||
// type SimpleConfig struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
//
|
||||
// // ReaderIO that reads port from SimpleConfig
|
||||
// getPort := readerio.Asks(func(c SimpleConfig) io.IO[int] {
|
||||
// return io.Of(c.Port)
|
||||
// })
|
||||
//
|
||||
// // Adapt DetailedConfig to SimpleConfig and convert int to string
|
||||
// simplify := func(d DetailedConfig) SimpleConfig {
|
||||
// return SimpleConfig{Host: d.Host, Port: d.Port}
|
||||
// }
|
||||
// toString := strconv.Itoa
|
||||
//
|
||||
// adapted := readerio.Promap(simplify, toString)(getPort)
|
||||
// result := adapted(DetailedConfig{Host: "localhost", Port: 8080, Debug: true})()
|
||||
// // result = "8080"
|
||||
//
|
||||
// Example - Logging with environment transformation:
|
||||
//
|
||||
// type AppEnv struct {
|
||||
// Logger *log.Logger
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// type LoggerEnv struct {
|
||||
// Logger *log.Logger
|
||||
// }
|
||||
//
|
||||
// logMessage := func(msg string) readerio.ReaderIO[LoggerEnv, func()] {
|
||||
// return readerio.Asks(func(env LoggerEnv) io.IO[func()] {
|
||||
// return io.Of(func() { env.Logger.Println(msg) })
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// extractLogger := func(app AppEnv) LoggerEnv {
|
||||
// return LoggerEnv{Logger: app.Logger}
|
||||
// }
|
||||
// ignore := func(func()) string { return "logged" }
|
||||
//
|
||||
// logAndReturn := readerio.Promap(extractLogger, ignore)(logMessage("Hello"))
|
||||
// // Now works with AppEnv and returns string instead of func()
|
||||
//
|
||||
//go:inline
|
||||
func Promap[E, A, D, B any](f func(D) E, g func(A) B) Kleisli[D, ReaderIO[E, A], B] {
|
||||
return reader.Promap(f, io.Map(g))
|
||||
}
|
||||
|
||||
// Local changes the value of the local environment during the execution of a ReaderIO.
|
||||
// This allows you to modify or adapt the environment before passing it to a ReaderIO computation.
|
||||
//
|
||||
// Local is particularly useful for:
|
||||
// - Extracting a subset of a larger environment
|
||||
// - Transforming environment types
|
||||
// - Providing different views of the same environment to different computations
|
||||
//
|
||||
// The transformation is contravariant - it transforms the input environment before
|
||||
// the ReaderIO computation sees it, but doesn't affect the output value.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The result type produced by the ReaderIO
|
||||
// - R2: The new input environment type
|
||||
// - R1: The original environment type expected by the ReaderIO
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the environment from R2 to R1
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderIO[R1, A] and returns a ReaderIO[R2, A]
|
||||
//
|
||||
// Example - Extracting a subset of environment:
|
||||
//
|
||||
// type AppConfig struct {
|
||||
// Database DatabaseConfig
|
||||
// Server ServerConfig
|
||||
// Logger *log.Logger
|
||||
// }
|
||||
//
|
||||
// type DatabaseConfig struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
//
|
||||
// // ReaderIO that only needs DatabaseConfig
|
||||
// connectDB := readerio.Asks(func(cfg DatabaseConfig) io.IO[string] {
|
||||
// return io.Of(fmt.Sprintf("Connected to %s:%d", cfg.Host, cfg.Port))
|
||||
// })
|
||||
//
|
||||
// // Extract database config from full app config
|
||||
// extractDB := func(app AppConfig) DatabaseConfig {
|
||||
// return app.Database
|
||||
// }
|
||||
//
|
||||
// // Adapt to work with full AppConfig
|
||||
// connectWithAppConfig := readerio.Local(extractDB)(connectDB)
|
||||
// result := connectWithAppConfig(AppConfig{
|
||||
// Database: DatabaseConfig{Host: "localhost", Port: 5432},
|
||||
// })()
|
||||
// // result = "Connected to localhost:5432"
|
||||
//
|
||||
// Example - Providing different views:
|
||||
//
|
||||
// type FullEnv struct {
|
||||
// UserID int
|
||||
// Role string
|
||||
// }
|
||||
//
|
||||
// type UserEnv struct {
|
||||
// UserID int
|
||||
// }
|
||||
//
|
||||
// getUserData := readerio.Asks(func(env UserEnv) io.IO[string] {
|
||||
// return io.Of(fmt.Sprintf("User: %d", env.UserID))
|
||||
// })
|
||||
//
|
||||
// toUserEnv := func(full FullEnv) UserEnv {
|
||||
// return UserEnv{UserID: full.UserID}
|
||||
// }
|
||||
//
|
||||
// adapted := readerio.Local(toUserEnv)(getUserData)
|
||||
// result := adapted(FullEnv{UserID: 42, Role: "admin"})()
|
||||
// // result = "User: 42"
|
||||
//
|
||||
//go:inline
|
||||
func Local[A, R1, R2 any](f func(R2) R1) Kleisli[R2, ReaderIO[R1, A], A] {
|
||||
return reader.Local[IO[A]](f)
|
||||
}
|
||||
|
||||
// Contramap is an alias for Local.
|
||||
// It changes the value of the local environment during the execution of a ReaderIO.
|
||||
// This is the contravariant functor operation that transforms the input environment.
|
||||
//
|
||||
// Contramap is semantically identical to Local - both modify the environment before
|
||||
// passing it to a ReaderIO. The name "Contramap" emphasizes the contravariant nature
|
||||
// of the transformation (transforming the input rather than the output).
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The result type produced by the ReaderIO
|
||||
// - R2: The new input environment type
|
||||
// - R1: The original environment type expected by the ReaderIO
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the environment from R2 to R1
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderIO[R1, A] and returns a ReaderIO[R2, A]
|
||||
//
|
||||
// Example - Environment adaptation:
|
||||
//
|
||||
// type DetailedEnv struct {
|
||||
// Config Config
|
||||
// Logger *log.Logger
|
||||
// Metrics Metrics
|
||||
// }
|
||||
//
|
||||
// type SimpleEnv struct {
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// readConfig := readerio.Asks(func(env SimpleEnv) io.IO[string] {
|
||||
// return io.Of(env.Config.Value)
|
||||
// })
|
||||
//
|
||||
// simplify := func(detailed DetailedEnv) SimpleEnv {
|
||||
// return SimpleEnv{Config: detailed.Config}
|
||||
// }
|
||||
//
|
||||
// adapted := readerio.Contramap(simplify)(readConfig)
|
||||
// result := adapted(DetailedEnv{Config: Config{Value: "test"}})()
|
||||
// // result = "test"
|
||||
//
|
||||
// See also: Local
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[A, R1, R2 any](f func(R2) R1) Kleisli[R2, ReaderIO[R1, A], A] {
|
||||
return reader.Contramap[IO[A]](f)
|
||||
}
|
||||
614
v2/readerio/profunctor_test.go
Normal file
614
v2/readerio/profunctor_test.go
Normal file
@@ -0,0 +1,614 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Test environment types
|
||||
type DetailedConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
Debug bool
|
||||
}
|
||||
|
||||
type SimpleConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
Database DatabaseConfig
|
||||
Server ServerConfig
|
||||
LogLevel string
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Port int
|
||||
Timeout int
|
||||
}
|
||||
|
||||
type UserEnv struct {
|
||||
UserID int
|
||||
}
|
||||
|
||||
type FullEnv struct {
|
||||
UserID int
|
||||
Role string
|
||||
}
|
||||
|
||||
// TestPromapBasic tests basic Promap functionality
|
||||
func TestPromapBasic(t *testing.T) {
|
||||
t.Run("transform both input and output", func(t *testing.T) {
|
||||
// ReaderIO that reads port from SimpleConfig
|
||||
getPort := func(c SimpleConfig) IO[int] {
|
||||
return io.Of(c.Port)
|
||||
}
|
||||
|
||||
// Adapt DetailedConfig to SimpleConfig and convert int to string
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Host: d.Host, Port: d.Port}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap(simplify, toString)(getPort)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 8080, Debug: true})()
|
||||
|
||||
assert.Equal(t, "8080", result)
|
||||
})
|
||||
|
||||
t.Run("identity transformations", func(t *testing.T) {
|
||||
getValue := func(n int) IO[int] {
|
||||
return io.Of(n * 2)
|
||||
}
|
||||
|
||||
// Identity functions should not change behavior
|
||||
identity := reader.Ask[int]()
|
||||
adapted := Promap(identity, identity)(getValue)
|
||||
result := adapted(5)()
|
||||
|
||||
assert.Equal(t, 10, result)
|
||||
})
|
||||
|
||||
t.Run("compose multiple transformations", func(t *testing.T) {
|
||||
getPort := func(c SimpleConfig) IO[int] {
|
||||
return io.Of(c.Port)
|
||||
}
|
||||
|
||||
// First transformation
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Host: d.Host, Port: d.Port}
|
||||
}
|
||||
double := N.Mul(2)
|
||||
|
||||
step1 := Promap(simplify, double)(getPort)
|
||||
|
||||
// Second transformation
|
||||
addDebug := func(d DetailedConfig) DetailedConfig {
|
||||
d.Debug = true
|
||||
return d
|
||||
}
|
||||
toString := S.Format[int]("Port: %d")
|
||||
|
||||
step2 := Promap(addDebug, toString)(step1)
|
||||
result := step2(DetailedConfig{Host: "localhost", Port: 8080})()
|
||||
|
||||
assert.Equal(t, "Port: 16160", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapWithIO tests Promap with actual IO effects
|
||||
func TestPromapWithIO(t *testing.T) {
|
||||
t.Run("transform IO result", func(t *testing.T) {
|
||||
counter := 0
|
||||
getAndIncrement := func(n int) IO[int] {
|
||||
return func() int {
|
||||
counter++
|
||||
return n + counter
|
||||
}
|
||||
}
|
||||
|
||||
double := reader.Ask[int]()
|
||||
toString := S.Format[int]("Result: %d")
|
||||
|
||||
adapted := Promap(double, toString)(getAndIncrement)
|
||||
result := adapted(10)()
|
||||
|
||||
assert.Equal(t, "Result: 11", result)
|
||||
assert.Equal(t, 1, counter)
|
||||
})
|
||||
|
||||
t.Run("environment transformation with side effects", func(t *testing.T) {
|
||||
var log []string
|
||||
|
||||
logAndReturn := func(msg string) IO[string] {
|
||||
return func() string {
|
||||
log = append(log, msg)
|
||||
return msg
|
||||
}
|
||||
}
|
||||
|
||||
addPrefix := S.Prepend("Input: ")
|
||||
addSuffix := S.Append(" [processed]")
|
||||
|
||||
adapted := Promap(addPrefix, addSuffix)(logAndReturn)
|
||||
result := adapted("test")()
|
||||
|
||||
assert.Equal(t, "Input: test [processed]", result)
|
||||
assert.Equal(t, []string{"Input: test"}, log)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapEnvironmentExtraction tests extracting subsets of environments
|
||||
func TestPromapEnvironmentExtraction(t *testing.T) {
|
||||
t.Run("extract database config", func(t *testing.T) {
|
||||
connectDB := func(cfg DatabaseConfig) IO[string] {
|
||||
return io.Of(fmt.Sprintf("Connected to %s:%d", cfg.Host, cfg.Port))
|
||||
}
|
||||
|
||||
extractDB := func(app AppConfig) DatabaseConfig {
|
||||
return app.Database
|
||||
}
|
||||
identity := reader.Ask[string]()
|
||||
|
||||
adapted := Promap(extractDB, identity)(connectDB)
|
||||
result := adapted(AppConfig{
|
||||
Database: DatabaseConfig{Host: "localhost", Port: 5432},
|
||||
Server: ServerConfig{Port: 8080, Timeout: 30},
|
||||
})()
|
||||
|
||||
assert.Equal(t, "Connected to localhost:5432", result)
|
||||
})
|
||||
|
||||
t.Run("extract and transform", func(t *testing.T) {
|
||||
getServerPort := func(cfg ServerConfig) IO[int] {
|
||||
return io.Of(cfg.Port)
|
||||
}
|
||||
|
||||
extractServer := func(app AppConfig) ServerConfig {
|
||||
return app.Server
|
||||
}
|
||||
formatPort := func(port int) string {
|
||||
return fmt.Sprintf("Server listening on port %d", port)
|
||||
}
|
||||
|
||||
adapted := Promap(extractServer, formatPort)(getServerPort)
|
||||
result := adapted(AppConfig{
|
||||
Server: ServerConfig{Port: 8080, Timeout: 30},
|
||||
})()
|
||||
|
||||
assert.Equal(t, "Server listening on port 8080", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalBasic tests basic Local functionality
|
||||
func TestLocalBasic(t *testing.T) {
|
||||
t.Run("extract subset of environment", func(t *testing.T) {
|
||||
connectDB := func(cfg DatabaseConfig) IO[string] {
|
||||
return io.Of(fmt.Sprintf("Connected to %s:%d", cfg.Host, cfg.Port))
|
||||
}
|
||||
|
||||
extractDB := func(app AppConfig) DatabaseConfig {
|
||||
return app.Database
|
||||
}
|
||||
|
||||
adapted := Local[string](extractDB)(connectDB)
|
||||
result := adapted(AppConfig{
|
||||
Database: DatabaseConfig{Host: "localhost", Port: 5432},
|
||||
})()
|
||||
|
||||
assert.Equal(t, "Connected to localhost:5432", result)
|
||||
})
|
||||
|
||||
t.Run("transform environment type", func(t *testing.T) {
|
||||
getUserData := func(env UserEnv) IO[string] {
|
||||
return io.Of(fmt.Sprintf("User: %d", env.UserID))
|
||||
}
|
||||
|
||||
toUserEnv := func(full FullEnv) UserEnv {
|
||||
return UserEnv{UserID: full.UserID}
|
||||
}
|
||||
|
||||
adapted := Local[string](toUserEnv)(getUserData)
|
||||
result := adapted(FullEnv{UserID: 42, Role: "admin"})()
|
||||
|
||||
assert.Equal(t, "User: 42", result)
|
||||
})
|
||||
|
||||
t.Run("identity transformation", func(t *testing.T) {
|
||||
getValue := func(n int) IO[int] {
|
||||
return io.Of(n * 2)
|
||||
}
|
||||
|
||||
identity := reader.Ask[int]()
|
||||
adapted := Local[int](identity)(getValue)
|
||||
result := adapted(5)()
|
||||
|
||||
assert.Equal(t, 10, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalComposition tests composing Local transformations
|
||||
func TestLocalComposition(t *testing.T) {
|
||||
t.Run("compose two Local transformations", func(t *testing.T) {
|
||||
getPort := func(cfg DatabaseConfig) IO[int] {
|
||||
return io.Of(cfg.Port)
|
||||
}
|
||||
|
||||
extractDB := func(app AppConfig) DatabaseConfig {
|
||||
return app.Database
|
||||
}
|
||||
|
||||
// First Local
|
||||
step1 := Local[int](extractDB)(getPort)
|
||||
|
||||
// Second Local - add default values
|
||||
addDefaults := func(app AppConfig) AppConfig {
|
||||
if app.Database.Host == "" {
|
||||
app.Database.Host = "localhost"
|
||||
}
|
||||
return app
|
||||
}
|
||||
|
||||
step2 := Local[int](addDefaults)(step1)
|
||||
result := step2(AppConfig{
|
||||
Database: DatabaseConfig{Host: "", Port: 5432},
|
||||
})()
|
||||
|
||||
assert.Equal(t, 5432, result)
|
||||
})
|
||||
|
||||
t.Run("chain multiple environment transformations", func(t *testing.T) {
|
||||
getHost := func(cfg SimpleConfig) IO[string] {
|
||||
return io.Of(cfg.Host)
|
||||
}
|
||||
|
||||
// Transform DetailedConfig -> SimpleConfig
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Host: d.Host, Port: d.Port}
|
||||
}
|
||||
|
||||
adapted := Local[string](simplify)(getHost)
|
||||
result := adapted(DetailedConfig{Host: "example.com", Port: 8080, Debug: true})()
|
||||
|
||||
assert.Equal(t, "example.com", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalWithIO tests Local with IO effects
|
||||
func TestLocalWithIO(t *testing.T) {
|
||||
t.Run("environment transformation with side effects", func(t *testing.T) {
|
||||
var accessLog []int
|
||||
|
||||
logAccess := func(id int) IO[string] {
|
||||
return func() string {
|
||||
accessLog = append(accessLog, id)
|
||||
return fmt.Sprintf("Accessed: %d", id)
|
||||
}
|
||||
}
|
||||
|
||||
extractUserID := func(env FullEnv) int {
|
||||
return env.UserID
|
||||
}
|
||||
|
||||
adapted := Local[string](extractUserID)(logAccess)
|
||||
result := adapted(FullEnv{UserID: 123, Role: "user"})()
|
||||
|
||||
assert.Equal(t, "Accessed: 123", result)
|
||||
assert.Equal(t, []int{123}, accessLog)
|
||||
})
|
||||
|
||||
t.Run("multiple executions with different environments", func(t *testing.T) {
|
||||
counter := 0
|
||||
increment := func(n int) IO[int] {
|
||||
return func() int {
|
||||
counter++
|
||||
return n + counter
|
||||
}
|
||||
}
|
||||
|
||||
double := N.Mul(2)
|
||||
adapted := Local[int](double)(increment)
|
||||
|
||||
result1 := adapted(5)() // 10 + 1 = 11
|
||||
result2 := adapted(10)() // 20 + 2 = 22
|
||||
|
||||
assert.Equal(t, 11, result1)
|
||||
assert.Equal(t, 22, result2)
|
||||
assert.Equal(t, 2, counter)
|
||||
})
|
||||
}
|
||||
|
||||
// TestContramapBasic tests basic Contramap functionality
|
||||
func TestContramapBasic(t *testing.T) {
|
||||
t.Run("environment adaptation", func(t *testing.T) {
|
||||
readConfig := func(env SimpleConfig) IO[string] {
|
||||
return io.Of(fmt.Sprintf("%s:%d", env.Host, env.Port))
|
||||
}
|
||||
|
||||
simplify := func(detailed DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Host: detailed.Host, Port: detailed.Port}
|
||||
}
|
||||
|
||||
adapted := Contramap[string](simplify)(readConfig)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 8080, Debug: true})()
|
||||
|
||||
assert.Equal(t, "localhost:8080", result)
|
||||
})
|
||||
|
||||
t.Run("extract field from larger structure", func(t *testing.T) {
|
||||
getPort := func(port int) IO[string] {
|
||||
return io.Of(fmt.Sprintf("Port: %d", port))
|
||||
}
|
||||
|
||||
extractPort := func(cfg SimpleConfig) int {
|
||||
return cfg.Port
|
||||
}
|
||||
|
||||
adapted := Contramap[string](extractPort)(getPort)
|
||||
result := adapted(SimpleConfig{Host: "localhost", Port: 9000})()
|
||||
|
||||
assert.Equal(t, "Port: 9000", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestContramapVsLocal verifies Contramap and Local are equivalent
|
||||
func TestContramapVsLocal(t *testing.T) {
|
||||
t.Run("same behavior as Local", func(t *testing.T) {
|
||||
getValue := func(n int) IO[int] {
|
||||
return io.Of(n * 3)
|
||||
}
|
||||
|
||||
double := N.Mul(2)
|
||||
|
||||
localResult := Local[int](double)(getValue)(5)()
|
||||
contramapResult := Contramap[int](double)(getValue)(5)()
|
||||
|
||||
assert.Equal(t, localResult, contramapResult)
|
||||
assert.Equal(t, 30, localResult) // (5 * 2) * 3 = 30
|
||||
})
|
||||
|
||||
t.Run("environment extraction equivalence", func(t *testing.T) {
|
||||
getHost := func(cfg SimpleConfig) IO[string] {
|
||||
return io.Of(cfg.Host)
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Host: d.Host, Port: d.Port}
|
||||
}
|
||||
|
||||
env := DetailedConfig{Host: "example.com", Port: 8080, Debug: false}
|
||||
|
||||
localResult := Local[string](simplify)(getHost)(env)()
|
||||
contramapResult := Contramap[string](simplify)(getHost)(env)()
|
||||
|
||||
assert.Equal(t, localResult, contramapResult)
|
||||
assert.Equal(t, "example.com", localResult)
|
||||
})
|
||||
}
|
||||
|
||||
// TestProfunctorLaws tests profunctor laws
|
||||
func TestProfunctorLaws(t *testing.T) {
|
||||
t.Run("identity law", func(t *testing.T) {
|
||||
getValue := func(n int) IO[int] {
|
||||
return io.Of(n + 10)
|
||||
}
|
||||
|
||||
identity := reader.Ask[int]()
|
||||
|
||||
// Promap(id, id) should be equivalent to id
|
||||
adapted := Promap(identity, identity)(getValue)
|
||||
original := getValue(5)()
|
||||
transformed := adapted(5)()
|
||||
|
||||
assert.Equal(t, original, transformed)
|
||||
assert.Equal(t, 15, transformed)
|
||||
})
|
||||
|
||||
t.Run("composition law", func(t *testing.T) {
|
||||
getValue := func(n int) IO[int] {
|
||||
return io.Of(n * 2)
|
||||
}
|
||||
|
||||
f1 := N.Add(1)
|
||||
f2 := N.Mul(3)
|
||||
g1 := N.Sub(5)
|
||||
g2 := N.Mul(2)
|
||||
|
||||
// Promap(f1, g2) . Promap(f2, g1) should equal Promap(f2 . f1, g2 . g1)
|
||||
// Note: composition order is reversed for contravariant part
|
||||
step1 := Promap(f2, g1)(getValue)
|
||||
composed1 := Promap(f1, g2)(step1)
|
||||
|
||||
composed2 := Promap(
|
||||
func(x int) int { return f2(f1(x)) },
|
||||
func(x int) int { return g2(g1(x)) },
|
||||
)(getValue)
|
||||
|
||||
result1 := composed1(10)()
|
||||
result2 := composed2(10)()
|
||||
|
||||
assert.Equal(t, result1, result2)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEdgeCases tests edge cases and special scenarios
|
||||
func TestEdgeCases(t *testing.T) {
|
||||
t.Run("empty struct environment", func(t *testing.T) {
|
||||
type Empty struct{}
|
||||
|
||||
getValue := func(e Empty) IO[int] {
|
||||
return io.Of(42)
|
||||
}
|
||||
|
||||
identity := reader.Ask[Empty]()
|
||||
adapted := Local[int](identity)(getValue)
|
||||
result := adapted(Empty{})()
|
||||
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
|
||||
t.Run("function type handling", func(t *testing.T) {
|
||||
getFunc := func(n int) IO[func(int) int] {
|
||||
return io.Of(N.Mul(2))
|
||||
}
|
||||
|
||||
double := N.Mul(2)
|
||||
applyFunc := reader.Read[int](5)
|
||||
|
||||
adapted := Promap(double, applyFunc)(getFunc)
|
||||
result := adapted(3)() // (3 * 2) = 6, then func(5) = 10
|
||||
|
||||
assert.Equal(t, 10, result)
|
||||
})
|
||||
|
||||
t.Run("complex nested transformations", func(t *testing.T) {
|
||||
type Level3 struct{ Value int }
|
||||
type Level2 struct{ L3 Level3 }
|
||||
type Level1 struct{ L2 Level2 }
|
||||
|
||||
getValue := func(l3 Level3) IO[int] {
|
||||
return io.Of(l3.Value)
|
||||
}
|
||||
|
||||
extract := func(l1 Level1) Level3 {
|
||||
return l1.L2.L3
|
||||
}
|
||||
multiply := N.Mul(10)
|
||||
|
||||
adapted := Promap(extract, multiply)(getValue)
|
||||
result := adapted(Level1{L2: Level2{L3: Level3{Value: 7}}})()
|
||||
|
||||
assert.Equal(t, 70, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestRealWorldScenarios tests practical use cases
|
||||
func TestRealWorldScenarios(t *testing.T) {
|
||||
t.Run("database connection with config extraction", func(t *testing.T) {
|
||||
type DBConfig struct {
|
||||
ConnectionString string
|
||||
}
|
||||
|
||||
type AppSettings struct {
|
||||
DB DBConfig
|
||||
APIKey string
|
||||
Timeout int
|
||||
}
|
||||
|
||||
connect := func(cfg DBConfig) IO[string] {
|
||||
return io.Of("Connected: " + cfg.ConnectionString)
|
||||
}
|
||||
|
||||
extractDB := func(settings AppSettings) DBConfig {
|
||||
return settings.DB
|
||||
}
|
||||
|
||||
adapted := Local[string](extractDB)(connect)
|
||||
result := adapted(AppSettings{
|
||||
DB: DBConfig{ConnectionString: "postgres://localhost"},
|
||||
APIKey: "secret",
|
||||
Timeout: 30,
|
||||
})()
|
||||
|
||||
assert.Equal(t, "Connected: postgres://localhost", result)
|
||||
})
|
||||
|
||||
t.Run("logging with environment transformation", func(t *testing.T) {
|
||||
type LogContext struct {
|
||||
RequestID string
|
||||
UserID int
|
||||
}
|
||||
|
||||
type RequestContext struct {
|
||||
RequestID string
|
||||
UserID int
|
||||
Path string
|
||||
Method string
|
||||
}
|
||||
|
||||
var logs []string
|
||||
logMessage := func(ctx LogContext) IO[func()] {
|
||||
return func() func() {
|
||||
return func() {
|
||||
logs = append(logs, fmt.Sprintf("[%s] User %d", ctx.RequestID, ctx.UserID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extractLogContext := func(req RequestContext) LogContext {
|
||||
return LogContext{RequestID: req.RequestID, UserID: req.UserID}
|
||||
}
|
||||
|
||||
adapted := Local[func()](extractLogContext)(logMessage)
|
||||
result := adapted(RequestContext{
|
||||
RequestID: "req-123",
|
||||
UserID: 42,
|
||||
Path: "/api/users",
|
||||
Method: "GET",
|
||||
})()
|
||||
|
||||
result()
|
||||
assert.Equal(t, []string{"[req-123] User 42"}, logs)
|
||||
})
|
||||
|
||||
t.Run("API response transformation", func(t *testing.T) {
|
||||
type APIResponse struct {
|
||||
StatusCode int
|
||||
Body string
|
||||
}
|
||||
|
||||
type EnrichedResponse struct {
|
||||
Response APIResponse
|
||||
Timestamp int64
|
||||
RequestID string
|
||||
}
|
||||
|
||||
formatResponse := func(resp APIResponse) IO[string] {
|
||||
return io.Of(fmt.Sprintf("Status: %d, Body: %s", resp.StatusCode, resp.Body))
|
||||
}
|
||||
|
||||
extractResponse := func(enriched EnrichedResponse) APIResponse {
|
||||
return enriched.Response
|
||||
}
|
||||
addMetadata := func(s string) string {
|
||||
return "[API] " + s
|
||||
}
|
||||
|
||||
adapted := Promap(extractResponse, addMetadata)(formatResponse)
|
||||
result := adapted(EnrichedResponse{
|
||||
Response: APIResponse{StatusCode: 200, Body: "OK"},
|
||||
Timestamp: 1234567890,
|
||||
RequestID: "req-456",
|
||||
})()
|
||||
|
||||
assert.Equal(t, "[API] Status: 200, Body: OK", result)
|
||||
})
|
||||
}
|
||||
@@ -174,7 +174,7 @@ func TestFilterOrElse_WithMap(t *testing.T) {
|
||||
onNegative := func(n int) string { return "negative number" }
|
||||
|
||||
filter := FilterOrElse[context.Context](isPositive, onNegative)
|
||||
double := Map[context.Context, string](func(n int) int { return n * 2 })
|
||||
double := Map[context.Context, string](N.Mul(2))
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
|
||||
76
v2/readerioeither/profunctor.go
Normal file
76
v2/readerioeither/profunctor.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioeither
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a ReaderIOEither.
|
||||
// It applies f to the input environment (contravariantly) and g to the output value (covariantly).
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// This operation allows you to:
|
||||
// - Adapt the environment type before passing it to the ReaderIOEither (via f)
|
||||
// - Transform the success value after the IO effect completes (via g)
|
||||
//
|
||||
// The error type E remains unchanged through the transformation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The original environment type expected by the ReaderIOEither
|
||||
// - E: The error type (unchanged)
|
||||
// - A: The original success type produced by the ReaderIOEither
|
||||
// - D: The new input environment type
|
||||
// - B: The new output success type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the input environment from D to R (contravariant)
|
||||
// - g: Function to transform the output success value from A to B (covariant)
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderIOEither[R, E, A] and returns a ReaderIOEither[D, E, B]
|
||||
//
|
||||
//go:inline
|
||||
func Promap[R, E, A, D, B any](f func(D) R, g func(A) B) Kleisli[D, E, ReaderIOEither[R, E, A], B] {
|
||||
return reader.Promap(f, ioeither.Map[E](g))
|
||||
}
|
||||
|
||||
// Contramap changes the value of the local environment during the execution of a ReaderIOEither.
|
||||
// This is the contravariant functor operation that transforms the input environment.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// Contramap is useful for adapting a ReaderIOEither to work with a different environment type
|
||||
// by providing a function that converts the new environment to the expected one.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The error type (unchanged)
|
||||
// - A: The success type (unchanged)
|
||||
// - R2: The new input environment type
|
||||
// - R1: The original environment type expected by the ReaderIOEither
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the environment from R2 to R1
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderIOEither[R1, E, A] and returns a ReaderIOEither[R2, E, A]
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[E, A, R1, R2 any](f func(R2) R1) Kleisli[R2, E, ReaderIOEither[R1, E, A], A] {
|
||||
return reader.Contramap[IOEither[E, A]](f)
|
||||
}
|
||||
133
v2/readerioeither/profunctor_test.go
Normal file
133
v2/readerioeither/profunctor_test.go
Normal file
@@ -0,0 +1,133 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioeither
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
IOE "github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type SimpleConfig struct {
|
||||
Port int
|
||||
}
|
||||
|
||||
type DetailedConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
// TestPromapBasic tests basic Promap functionality
|
||||
func TestPromapBasic(t *testing.T) {
|
||||
t.Run("transform both input and output", func(t *testing.T) {
|
||||
// ReaderIOEither that reads port from SimpleConfig
|
||||
getPort := func(c SimpleConfig) IOEither[string, int] {
|
||||
return IOE.Of[string](c.Port)
|
||||
}
|
||||
|
||||
// Transform DetailedConfig to SimpleConfig and int to string
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap[SimpleConfig, string](simplify, toString)(getPort)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 8080})()
|
||||
|
||||
assert.Equal(t, E.Of[string]("8080"), result)
|
||||
})
|
||||
|
||||
t.Run("handles error case", func(t *testing.T) {
|
||||
// ReaderIOEither that returns an error
|
||||
getError := func(c SimpleConfig) IOEither[string, int] {
|
||||
return IOE.Left[int]("error occurred")
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap[SimpleConfig, string](simplify, toString)(getError)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 8080})()
|
||||
|
||||
assert.Equal(t, E.Left[string]("error occurred"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestContramapBasic tests basic Contramap functionality
|
||||
func TestContramapBasic(t *testing.T) {
|
||||
t.Run("environment adaptation", func(t *testing.T) {
|
||||
// ReaderIOEither that reads from SimpleConfig
|
||||
getPort := func(c SimpleConfig) IOEither[string, int] {
|
||||
return IOE.Of[string](c.Port)
|
||||
}
|
||||
|
||||
// Adapt to work with DetailedConfig
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
|
||||
adapted := Contramap[string, int](simplify)(getPort)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 9000})()
|
||||
|
||||
assert.Equal(t, E.Of[string](9000), result)
|
||||
})
|
||||
|
||||
t.Run("preserves error", func(t *testing.T) {
|
||||
getError := func(c SimpleConfig) IOEither[string, int] {
|
||||
return IOE.Left[int]("config error")
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
|
||||
adapted := Contramap[string, int](simplify)(getError)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 9000})()
|
||||
|
||||
assert.Equal(t, E.Left[int]("config error"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapWithIO tests Promap with actual IO effects
|
||||
func TestPromapWithIO(t *testing.T) {
|
||||
t.Run("transform IO result", func(t *testing.T) {
|
||||
counter := 0
|
||||
|
||||
// ReaderIOEither with side effect
|
||||
getPortWithEffect := func(c SimpleConfig) IOEither[string, int] {
|
||||
return func() E.Either[string, int] {
|
||||
counter++
|
||||
return E.Of[string](c.Port)
|
||||
}
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap[SimpleConfig, string](simplify, toString)(getPortWithEffect)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 8080})()
|
||||
|
||||
assert.Equal(t, E.Of[string]("8080"), result)
|
||||
assert.Equal(t, 1, counter) // Side effect occurred
|
||||
})
|
||||
}
|
||||
@@ -182,7 +182,7 @@ func TestFilterOrElse_WithMap(t *testing.T) {
|
||||
onNegative := func(n int) error { return fmt.Errorf("negative number") }
|
||||
|
||||
filter := FilterOrElse[context.Context](isPositive, onNegative)
|
||||
double := Map[context.Context](func(n int) int { return n * 2 })
|
||||
double := Map[context.Context](N.Mul(2))
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
|
||||
73
v2/readerioresult/profunctor.go
Normal file
73
v2/readerioresult/profunctor.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
RIOE "github.com/IBM/fp-go/v2/readerioeither"
|
||||
)
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a ReaderIOResult.
|
||||
// It applies f to the input environment (contravariantly) and g to the output value (covariantly).
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// This operation allows you to:
|
||||
// - Adapt the environment type before passing it to the ReaderIOResult (via f)
|
||||
// - Transform the success value after the IO effect completes (via g)
|
||||
//
|
||||
// The error type is fixed as error and remains unchanged through the transformation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The original environment type expected by the ReaderIOResult
|
||||
// - A: The original success type produced by the ReaderIOResult
|
||||
// - D: The new input environment type
|
||||
// - B: The new output success type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the input environment from D to R (contravariant)
|
||||
// - g: Function to transform the output success value from A to B (covariant)
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderIOResult[R, A] and returns a ReaderIOResult[D, B]
|
||||
//
|
||||
//go:inline
|
||||
func Promap[R, A, D, B any](f func(D) R, g func(A) B) Kleisli[D, ReaderIOResult[R, A], B] {
|
||||
return RIOE.Promap[R, error](f, g)
|
||||
}
|
||||
|
||||
// Contramap changes the value of the local environment during the execution of a ReaderIOResult.
|
||||
// This is the contravariant functor operation that transforms the input environment.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// Contramap is useful for adapting a ReaderIOResult to work with a different environment type
|
||||
// by providing a function that converts the new environment to the expected one.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type (unchanged)
|
||||
// - R2: The new input environment type
|
||||
// - R1: The original environment type expected by the ReaderIOResult
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the environment from R2 to R1
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderIOResult[R1, A] and returns a ReaderIOResult[R2, A]
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[A, R1, R2 any](f func(R2) R1) Kleisli[R2, ReaderIOResult[R1, A], A] {
|
||||
return RIOE.Contramap[error, A](f)
|
||||
}
|
||||
98
v2/readerioresult/profunctor_test.go
Normal file
98
v2/readerioresult/profunctor_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
R "github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type SimpleConfig struct {
|
||||
Port int
|
||||
}
|
||||
|
||||
type DetailedConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
// TestPromapBasic tests basic Promap functionality
|
||||
func TestPromapBasic(t *testing.T) {
|
||||
t.Run("transform both input and output", func(t *testing.T) {
|
||||
// ReaderIOResult that reads port from SimpleConfig
|
||||
getPort := func(c SimpleConfig) IOResult[int] {
|
||||
return func() R.Result[int] {
|
||||
return R.Of(c.Port)
|
||||
}
|
||||
}
|
||||
|
||||
// Transform DetailedConfig to SimpleConfig and int to string
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap(simplify, toString)(getPort)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 8080})()
|
||||
|
||||
assert.Equal(t, R.Of("8080"), result)
|
||||
})
|
||||
|
||||
t.Run("handles error case", func(t *testing.T) {
|
||||
// ReaderIOResult that returns an error
|
||||
getError := func(c SimpleConfig) IOResult[int] {
|
||||
return func() R.Result[int] {
|
||||
return R.Left[int](fmt.Errorf("error occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap(simplify, toString)(getError)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 8080})()
|
||||
|
||||
assert.True(t, R.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestContramapBasic tests basic Contramap functionality
|
||||
func TestContramapBasic(t *testing.T) {
|
||||
t.Run("environment adaptation", func(t *testing.T) {
|
||||
// ReaderIOResult that reads from SimpleConfig
|
||||
getPort := func(c SimpleConfig) IOResult[int] {
|
||||
return func() R.Result[int] {
|
||||
return R.Of(c.Port)
|
||||
}
|
||||
}
|
||||
|
||||
// Adapt to work with DetailedConfig
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
|
||||
adapted := Contramap[int](simplify)(getPort)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 9000})()
|
||||
|
||||
assert.Equal(t, R.Of(9000), result)
|
||||
})
|
||||
}
|
||||
74
v2/readeroption/profunctor.go
Normal file
74
v2/readeroption/profunctor.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readeroption
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a ReaderOption.
|
||||
// It applies f to the input environment (contravariantly) and g to the output value (covariantly).
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// This operation allows you to:
|
||||
// - Adapt the environment type before passing it to the ReaderOption (via f)
|
||||
// - Transform the Some value after the computation completes (via g)
|
||||
//
|
||||
// The None case remains unchanged through the transformation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The original environment type expected by the ReaderOption
|
||||
// - A: The original value type produced by the ReaderOption
|
||||
// - D: The new input environment type
|
||||
// - B: The new output value type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the input environment from D to R (contravariant)
|
||||
// - g: Function to transform the output Some value from A to B (covariant)
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderOption[R, A] and returns a ReaderOption[D, B]
|
||||
//
|
||||
//go:inline
|
||||
func Promap[R, A, D, B any](f func(D) R, g func(A) B) Kleisli[D, ReaderOption[R, A], B] {
|
||||
return reader.Promap(f, option.Map(g))
|
||||
}
|
||||
|
||||
// Contramap changes the value of the local environment during the execution of a ReaderOption.
|
||||
// This is the contravariant functor operation that transforms the input environment.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// Contramap is useful for adapting a ReaderOption to work with a different environment type
|
||||
// by providing a function that converts the new environment to the expected one.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The value type (unchanged)
|
||||
// - R2: The new input environment type
|
||||
// - R1: The original environment type expected by the ReaderOption
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the environment from R2 to R1
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderOption[R1, A] and returns a ReaderOption[R2, A]
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[A, R1, R2 any](f func(R2) R1) Kleisli[R2, ReaderOption[R1, A], A] {
|
||||
return reader.Contramap[Option[A]](f)
|
||||
}
|
||||
106
v2/readeroption/profunctor_test.go
Normal file
106
v2/readeroption/profunctor_test.go
Normal file
@@ -0,0 +1,106 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readeroption
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type SimpleConfig struct {
|
||||
Port int
|
||||
}
|
||||
|
||||
type DetailedConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
// TestPromapBasic tests basic Promap functionality
|
||||
func TestPromapBasic(t *testing.T) {
|
||||
t.Run("transform both input and output with Some", func(t *testing.T) {
|
||||
// ReaderOption that reads port from SimpleConfig
|
||||
getPort := func(c SimpleConfig) Option[int] {
|
||||
return O.Of(c.Port)
|
||||
}
|
||||
|
||||
// Transform DetailedConfig to SimpleConfig and int to string
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap(simplify, toString)(getPort)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 8080})
|
||||
|
||||
assert.Equal(t, O.Of("8080"), result)
|
||||
})
|
||||
|
||||
t.Run("handles None case", func(t *testing.T) {
|
||||
// ReaderOption that returns None
|
||||
getNone := func(c SimpleConfig) Option[int] {
|
||||
return O.None[int]()
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap(simplify, toString)(getNone)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 8080})
|
||||
|
||||
assert.Equal(t, O.None[string](), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestContramapBasic tests basic Contramap functionality
|
||||
func TestContramapBasic(t *testing.T) {
|
||||
t.Run("environment adaptation", func(t *testing.T) {
|
||||
// ReaderOption that reads from SimpleConfig
|
||||
getPort := func(c SimpleConfig) Option[int] {
|
||||
return O.Of(c.Port)
|
||||
}
|
||||
|
||||
// Adapt to work with DetailedConfig
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
|
||||
adapted := Contramap[int](simplify)(getPort)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 9000})
|
||||
|
||||
assert.Equal(t, O.Of(9000), result)
|
||||
})
|
||||
|
||||
t.Run("preserves None", func(t *testing.T) {
|
||||
getNone := func(c SimpleConfig) Option[int] {
|
||||
return O.None[int]()
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
|
||||
adapted := Contramap[int](simplify)(getNone)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 9000})
|
||||
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
}
|
||||
@@ -320,7 +320,7 @@ func Flatten[E, A any](mma ReaderOption[E, ReaderOption[E, A]]) ReaderOption[E,
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func Local[A, R2, R1 any](f func(R2) R1) func(ReaderOption[R1, A]) ReaderOption[R2, A] {
|
||||
func Local[A, R1, R2 any](f func(R2) R1) func(ReaderOption[R1, A]) ReaderOption[R2, A] {
|
||||
return reader.Local[Option[A]](f)
|
||||
}
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ func TestFilterOrElse_WithMap(t *testing.T) {
|
||||
onNegative := func(n int) error { return fmt.Errorf("negative number") }
|
||||
|
||||
filter := FilterOrElse[Config](isPositive, onNegative)
|
||||
double := Map[Config](func(n int) int { return n * 2 })
|
||||
double := Map[Config](N.Mul(2))
|
||||
|
||||
cfg := Config{MaxValue: 100}
|
||||
|
||||
|
||||
73
v2/readerresult/profunctor.go
Normal file
73
v2/readerresult/profunctor.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerresult
|
||||
|
||||
import (
|
||||
RE "github.com/IBM/fp-go/v2/readereither"
|
||||
)
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a ReaderResult.
|
||||
// It applies f to the input environment (contravariantly) and g to the output value (covariantly).
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// This operation allows you to:
|
||||
// - Adapt the environment type before passing it to the ReaderResult (via f)
|
||||
// - Transform the success value after the computation completes (via g)
|
||||
//
|
||||
// The error type is fixed as error and remains unchanged through the transformation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The original environment type expected by the ReaderResult
|
||||
// - A: The original success type produced by the ReaderResult
|
||||
// - D: The new input environment type
|
||||
// - B: The new output success type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the input environment from D to R (contravariant)
|
||||
// - g: Function to transform the output success value from A to B (covariant)
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderResult[R, A] and returns a ReaderResult[D, B]
|
||||
//
|
||||
//go:inline
|
||||
func Promap[R, A, D, B any](f func(D) R, g func(A) B) Kleisli[D, ReaderResult[R, A], B] {
|
||||
return RE.Promap[R, error](f, g)
|
||||
}
|
||||
|
||||
// Contramap changes the value of the local environment during the execution of a ReaderResult.
|
||||
// This is the contravariant functor operation that transforms the input environment.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// Contramap is useful for adapting a ReaderResult to work with a different environment type
|
||||
// by providing a function that converts the new environment to the expected one.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type (unchanged)
|
||||
// - R2: The new input environment type
|
||||
// - R1: The original environment type expected by the ReaderResult
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the environment from R2 to R1
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderResult[R1, A] and returns a ReaderResult[R2, A]
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[A, R1, R2 any](f func(R2) R1) Kleisli[R2, ReaderResult[R1, A], A] {
|
||||
return RE.Contramap[error, A](f)
|
||||
}
|
||||
107
v2/readerresult/profunctor_test.go
Normal file
107
v2/readerresult/profunctor_test.go
Normal file
@@ -0,0 +1,107 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerresult
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
R "github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type SimpleConfig struct {
|
||||
Port int
|
||||
}
|
||||
|
||||
type DetailedConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
// TestPromapBasic tests basic Promap functionality
|
||||
func TestPromapBasic(t *testing.T) {
|
||||
t.Run("transform both input and output", func(t *testing.T) {
|
||||
// ReaderResult that reads port from SimpleConfig
|
||||
getPort := func(c SimpleConfig) Result[int] {
|
||||
return R.Of(c.Port)
|
||||
}
|
||||
|
||||
// Transform DetailedConfig to SimpleConfig and int to string
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap(simplify, toString)(getPort)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 8080})
|
||||
|
||||
assert.Equal(t, R.Of("8080"), result)
|
||||
})
|
||||
|
||||
t.Run("handles error case", func(t *testing.T) {
|
||||
// ReaderResult that returns an error
|
||||
getError := func(c SimpleConfig) Result[int] {
|
||||
return R.Left[int](fmt.Errorf("error occurred"))
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap(simplify, toString)(getError)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 8080})
|
||||
|
||||
assert.True(t, R.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestContramapBasic tests basic Contramap functionality
|
||||
func TestContramapBasic(t *testing.T) {
|
||||
t.Run("environment adaptation", func(t *testing.T) {
|
||||
// ReaderResult that reads from SimpleConfig
|
||||
getPort := func(c SimpleConfig) Result[int] {
|
||||
return R.Of(c.Port)
|
||||
}
|
||||
|
||||
// Adapt to work with DetailedConfig
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
|
||||
adapted := Contramap[int](simplify)(getPort)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 9000})
|
||||
|
||||
assert.Equal(t, R.Of(9000), result)
|
||||
})
|
||||
|
||||
t.Run("preserves error", func(t *testing.T) {
|
||||
getError := func(c SimpleConfig) Result[int] {
|
||||
return R.Left[int](fmt.Errorf("config error"))
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
|
||||
adapted := Contramap[int](simplify)(getError)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 9000})
|
||||
|
||||
assert.True(t, R.IsLeft(result))
|
||||
})
|
||||
}
|
||||
@@ -719,7 +719,7 @@ func BiMap[R, A, B any](f Endomorphism[error], g func(A) B) Operator[R, A, B] {
|
||||
// // adapted now accepts DB instead of Config
|
||||
//
|
||||
//go:inline
|
||||
func Local[A, R2, R1 any](f func(R2) R1) func(ReaderResult[R1, A]) ReaderResult[R2, A] {
|
||||
func Local[A, R1, R2 any](f func(R2) R1) func(ReaderResult[R1, A]) ReaderResult[R2, A] {
|
||||
return reader.Local[Result[A]](f)
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
// TestFirstMonoid tests the FirstMonoid implementation
|
||||
func TestFirstMonoid(t *testing.T) {
|
||||
zero := func() Result[int] { return Left[int](errors.New("empty")) }
|
||||
m := FirstMonoid[int](zero)
|
||||
m := FirstMonoid(zero)
|
||||
|
||||
t.Run("both Right values - returns first", func(t *testing.T) {
|
||||
result := m.Concat(Right(2), Right(3))
|
||||
@@ -94,7 +94,7 @@ func TestFirstMonoid(t *testing.T) {
|
||||
|
||||
t.Run("with strings", func(t *testing.T) {
|
||||
zeroStr := func() Result[string] { return Left[string](errors.New("empty")) }
|
||||
strMonoid := FirstMonoid[string](zeroStr)
|
||||
strMonoid := FirstMonoid(zeroStr)
|
||||
|
||||
result := strMonoid.Concat(Right("first"), Right("second"))
|
||||
assert.Equal(t, Right("first"), result)
|
||||
@@ -107,7 +107,7 @@ func TestFirstMonoid(t *testing.T) {
|
||||
// TestLastMonoid tests the LastMonoid implementation
|
||||
func TestLastMonoid(t *testing.T) {
|
||||
zero := func() Result[int] { return Left[int](errors.New("empty")) }
|
||||
m := LastMonoid[int](zero)
|
||||
m := LastMonoid(zero)
|
||||
|
||||
t.Run("both Right values - returns last", func(t *testing.T) {
|
||||
result := m.Concat(Right(2), Right(3))
|
||||
@@ -176,7 +176,7 @@ func TestLastMonoid(t *testing.T) {
|
||||
|
||||
t.Run("with strings", func(t *testing.T) {
|
||||
zeroStr := func() Result[string] { return Left[string](errors.New("empty")) }
|
||||
strMonoid := LastMonoid[string](zeroStr)
|
||||
strMonoid := LastMonoid(zeroStr)
|
||||
|
||||
result := strMonoid.Concat(Right("first"), Right("second"))
|
||||
assert.Equal(t, Right("second"), result)
|
||||
@@ -189,7 +189,7 @@ func TestLastMonoid(t *testing.T) {
|
||||
// TestAltMonoid tests the AltMonoid implementation
|
||||
func TestAltMonoid(t *testing.T) {
|
||||
zero := func() Result[int] { return Left[int](errors.New("empty")) }
|
||||
m := AltMonoid[int](zero)
|
||||
m := AltMonoid(zero)
|
||||
|
||||
t.Run("both Right values - returns first", func(t *testing.T) {
|
||||
result := m.Concat(Right(2), Right(3))
|
||||
@@ -251,8 +251,8 @@ func TestAltMonoid(t *testing.T) {
|
||||
// TestFirstMonoidVsAltMonoid verifies FirstMonoid and AltMonoid have the same behavior
|
||||
func TestFirstMonoidVsAltMonoid(t *testing.T) {
|
||||
zero := func() Result[int] { return Left[int](errors.New("empty")) }
|
||||
firstMonoid := FirstMonoid[int](zero)
|
||||
altMonoid := AltMonoid[int](zero)
|
||||
firstMonoid := FirstMonoid(zero)
|
||||
altMonoid := AltMonoid(zero)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
@@ -285,8 +285,8 @@ func TestFirstMonoidVsAltMonoid(t *testing.T) {
|
||||
// TestFirstMonoidVsLastMonoid verifies the difference between FirstMonoid and LastMonoid
|
||||
func TestFirstMonoidVsLastMonoid(t *testing.T) {
|
||||
zero := func() Result[int] { return Left[int](errors.New("empty")) }
|
||||
firstMonoid := FirstMonoid[int](zero)
|
||||
lastMonoid := LastMonoid[int](zero)
|
||||
firstMonoid := FirstMonoid(zero)
|
||||
lastMonoid := LastMonoid(zero)
|
||||
|
||||
t.Run("both Right - different results", func(t *testing.T) {
|
||||
firstResult := firstMonoid.Concat(Right(1), Right(2))
|
||||
@@ -341,7 +341,7 @@ func TestFirstMonoidVsLastMonoid(t *testing.T) {
|
||||
func TestMonoidLaws(t *testing.T) {
|
||||
t.Run("FirstMonoid laws", func(t *testing.T) {
|
||||
zero := func() Result[int] { return Left[int](errors.New("empty")) }
|
||||
m := FirstMonoid[int](zero)
|
||||
m := FirstMonoid(zero)
|
||||
|
||||
a := Right(1)
|
||||
b := Right(2)
|
||||
@@ -363,7 +363,7 @@ func TestMonoidLaws(t *testing.T) {
|
||||
|
||||
t.Run("LastMonoid laws", func(t *testing.T) {
|
||||
zero := func() Result[int] { return Left[int](errors.New("empty")) }
|
||||
m := LastMonoid[int](zero)
|
||||
m := LastMonoid(zero)
|
||||
|
||||
a := Right(1)
|
||||
b := Right(2)
|
||||
@@ -385,7 +385,7 @@ func TestMonoidLaws(t *testing.T) {
|
||||
|
||||
t.Run("AltMonoid laws", func(t *testing.T) {
|
||||
zero := func() Result[int] { return Left[int](errors.New("empty")) }
|
||||
m := AltMonoid[int](zero)
|
||||
m := AltMonoid(zero)
|
||||
|
||||
a := Right(1)
|
||||
b := Right(2)
|
||||
@@ -407,7 +407,7 @@ func TestMonoidLaws(t *testing.T) {
|
||||
|
||||
t.Run("FirstMonoid laws with Left values", func(t *testing.T) {
|
||||
zero := func() Result[int] { return Left[int](errors.New("empty")) }
|
||||
m := FirstMonoid[int](zero)
|
||||
m := FirstMonoid(zero)
|
||||
|
||||
a := Left[int](errors.New("err1"))
|
||||
b := Left[int](errors.New("err2"))
|
||||
@@ -421,7 +421,7 @@ func TestMonoidLaws(t *testing.T) {
|
||||
|
||||
t.Run("LastMonoid laws with Left values", func(t *testing.T) {
|
||||
zero := func() Result[int] { return Left[int](errors.New("empty")) }
|
||||
m := LastMonoid[int](zero)
|
||||
m := LastMonoid(zero)
|
||||
|
||||
a := Left[int](errors.New("err1"))
|
||||
b := Left[int](errors.New("err2"))
|
||||
@@ -438,7 +438,7 @@ func TestMonoidLaws(t *testing.T) {
|
||||
func TestMonoidEdgeCases(t *testing.T) {
|
||||
t.Run("FirstMonoid with empty concatenations", func(t *testing.T) {
|
||||
zero := func() Result[int] { return Left[int](errors.New("empty")) }
|
||||
m := FirstMonoid[int](zero)
|
||||
m := FirstMonoid(zero)
|
||||
|
||||
// Empty with empty
|
||||
result := m.Concat(m.Empty(), m.Empty())
|
||||
@@ -447,7 +447,7 @@ func TestMonoidEdgeCases(t *testing.T) {
|
||||
|
||||
t.Run("LastMonoid with empty concatenations", func(t *testing.T) {
|
||||
zero := func() Result[int] { return Left[int](errors.New("empty")) }
|
||||
m := LastMonoid[int](zero)
|
||||
m := LastMonoid(zero)
|
||||
|
||||
// Empty with empty
|
||||
result := m.Concat(m.Empty(), m.Empty())
|
||||
@@ -456,7 +456,7 @@ func TestMonoidEdgeCases(t *testing.T) {
|
||||
|
||||
t.Run("FirstMonoid chain of operations", func(t *testing.T) {
|
||||
zero := func() Result[int] { return Left[int](errors.New("empty")) }
|
||||
m := FirstMonoid[int](zero)
|
||||
m := FirstMonoid(zero)
|
||||
|
||||
// Chain multiple operations
|
||||
result := m.Concat(
|
||||
@@ -471,7 +471,7 @@ func TestMonoidEdgeCases(t *testing.T) {
|
||||
|
||||
t.Run("LastMonoid chain of operations", func(t *testing.T) {
|
||||
zero := func() Result[int] { return Left[int](errors.New("empty")) }
|
||||
m := LastMonoid[int](zero)
|
||||
m := LastMonoid(zero)
|
||||
|
||||
// Chain multiple operations
|
||||
result := m.Concat(
|
||||
@@ -486,7 +486,7 @@ func TestMonoidEdgeCases(t *testing.T) {
|
||||
|
||||
t.Run("AltMonoid chain of operations", func(t *testing.T) {
|
||||
zero := func() Result[int] { return Left[int](errors.New("empty")) }
|
||||
m := AltMonoid[int](zero)
|
||||
m := AltMonoid(zero)
|
||||
|
||||
// Chain multiple operations - should return first Right
|
||||
result := m.Concat(
|
||||
|
||||
@@ -89,7 +89,7 @@ FunctionSemigroup - Lifts a semigroup to work with functions:
|
||||
// Lift to functions that return integers
|
||||
funcSG := SG.FunctionSemigroup[string](intSum)
|
||||
|
||||
f := func(s string) int { return len(s) }
|
||||
f := S.Size
|
||||
g := func(s string) int { return len(s) * 2 }
|
||||
|
||||
// Combine functions
|
||||
|
||||
@@ -78,7 +78,7 @@ func Reverse[A any](m Semigroup[A]) Semigroup[A] {
|
||||
// intSum := N.SemigroupSum[int]()
|
||||
// funcSG := semigroup.FunctionSemigroup[string](intSum)
|
||||
//
|
||||
// f := func(s string) int { return len(s) }
|
||||
// f := S.Size
|
||||
// g := func(s string) int { return len(s) * 2 }
|
||||
// combined := funcSG.Concat(f, g)
|
||||
// result := combined("hello") // 5 + 10 = 15
|
||||
|
||||
147
v2/state/monoid.go
Normal file
147
v2/state/monoid.go
Normal file
@@ -0,0 +1,147 @@
|
||||
// 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 state
|
||||
|
||||
import (
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
)
|
||||
|
||||
// ApplicativeMonoid lifts a monoid into the State applicative functor context.
|
||||
//
|
||||
// This function creates a monoid for State[S, A] values given a monoid for the base type A.
|
||||
// It uses the State monad's applicative operations (Of, MonadMap, MonadAp) to lift the
|
||||
// monoid operations into the State context, allowing you to combine stateful computations
|
||||
// that produce monoidal values.
|
||||
//
|
||||
// The resulting monoid combines State computations by:
|
||||
// 1. Threading the state through both computations sequentially
|
||||
// 2. Combining the produced values using the underlying monoid's Concat operation
|
||||
// 3. Returning a new State computation with the combined value and final state
|
||||
//
|
||||
// The empty element is a State computation that returns the underlying monoid's empty value
|
||||
// without modifying the state.
|
||||
//
|
||||
// This is particularly useful for:
|
||||
// - Accumulating results across multiple stateful computations
|
||||
// - Building complex state transformations that aggregate values
|
||||
// - Combining independent stateful operations that produce monoidal results
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The state type that is threaded through the computations
|
||||
// - A: The value type that has a monoid structure
|
||||
//
|
||||
// Parameters:
|
||||
// - m: A monoid for the base type A that defines how to combine values
|
||||
//
|
||||
// Returns:
|
||||
// - A Monoid[State[S, A]] that combines stateful computations using the base monoid
|
||||
//
|
||||
// The resulting monoid satisfies the standard monoid laws:
|
||||
// - Associativity: Concat(Concat(s1, s2), s3) = Concat(s1, Concat(s2, s3))
|
||||
// - Left identity: Concat(Empty(), s) = s
|
||||
// - Right identity: Concat(s, Empty()) = s
|
||||
//
|
||||
// Example with integer addition:
|
||||
//
|
||||
// import (
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// "github.com/IBM/fp-go/v2/pair"
|
||||
// )
|
||||
//
|
||||
// type Counter struct {
|
||||
// count int
|
||||
// }
|
||||
//
|
||||
// // Create a monoid for State[Counter, int] using integer addition
|
||||
// intAddMonoid := N.MonoidSum[int]()
|
||||
// stateMonoid := state.ApplicativeMonoid[Counter](intAddMonoid)
|
||||
//
|
||||
// // Create two stateful computations
|
||||
// s1 := state.Of[Counter](5) // Returns 5, state unchanged
|
||||
// s2 := state.Of[Counter](3) // Returns 3, state unchanged
|
||||
//
|
||||
// // Combine them using the monoid
|
||||
// combined := stateMonoid.Concat(s1, s2)
|
||||
// result := combined(Counter{count: 10})
|
||||
// // result = Pair{head: Counter{count: 10}, tail: 8} // 5 + 3
|
||||
//
|
||||
// // Empty element
|
||||
// empty := stateMonoid.Empty()
|
||||
// emptyResult := empty(Counter{count: 10})
|
||||
// // emptyResult = Pair{head: Counter{count: 10}, tail: 0}
|
||||
//
|
||||
// Example with string concatenation and state modification:
|
||||
//
|
||||
// import (
|
||||
// S "github.com/IBM/fp-go/v2/string"
|
||||
// )
|
||||
//
|
||||
// type Logger struct {
|
||||
// logs []string
|
||||
// }
|
||||
//
|
||||
// strMonoid := S.Monoid
|
||||
// stateMonoid := state.ApplicativeMonoid[Logger](strMonoid)
|
||||
//
|
||||
// // Stateful computation that logs and returns a message
|
||||
// logMessage := func(msg string) state.State[Logger, string] {
|
||||
// return func(s Logger) pair.Pair[Logger, string] {
|
||||
// newState := Logger{logs: append(s.logs, msg)}
|
||||
// return pair.MakePair(newState, msg)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// s1 := logMessage("Hello")
|
||||
// s2 := logMessage(" World")
|
||||
//
|
||||
// // Combine the computations - both log entries are added, messages concatenated
|
||||
// combined := stateMonoid.Concat(s1, s2)
|
||||
// result := combined(Logger{logs: []string{}})
|
||||
// // result.head.logs = ["Hello", " World"]
|
||||
// // result.tail = "Hello World"
|
||||
//
|
||||
// Example demonstrating monoid laws:
|
||||
//
|
||||
// intAddMonoid := N.MonoidSum[int]()
|
||||
// m := state.ApplicativeMonoid[Counter](intAddMonoid)
|
||||
//
|
||||
// s1 := state.Of[Counter](1)
|
||||
// s2 := state.Of[Counter](2)
|
||||
// s3 := state.Of[Counter](3)
|
||||
//
|
||||
// initialState := Counter{count: 0}
|
||||
//
|
||||
// // Associativity
|
||||
// left := m.Concat(m.Concat(s1, s2), s3)
|
||||
// right := m.Concat(s1, m.Concat(s2, s3))
|
||||
// // Both produce: Pair{head: Counter{count: 0}, tail: 6}
|
||||
//
|
||||
// // Left identity
|
||||
// leftId := m.Concat(m.Empty(), s1)
|
||||
// // Produces: Pair{head: Counter{count: 0}, tail: 1}
|
||||
//
|
||||
// // Right identity
|
||||
// rightId := m.Concat(s1, m.Empty())
|
||||
// // Produces: Pair{head: Counter{count: 0}, tail: 1}
|
||||
//
|
||||
//go:inline
|
||||
func ApplicativeMonoid[S, A any](m M.Monoid[A]) M.Monoid[State[S, A]] {
|
||||
return M.ApplicativeMonoid(
|
||||
Of[S, A],
|
||||
MonadMap[S, func(A) func(A) A, A, func(A) A],
|
||||
MonadAp[A, S, A],
|
||||
m)
|
||||
}
|
||||
552
v2/state/monoid_test.go
Normal file
552
v2/state/monoid_test.go
Normal file
@@ -0,0 +1,552 @@
|
||||
// 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 state
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Counter is a simple state type for testing
|
||||
type Counter struct {
|
||||
count int
|
||||
}
|
||||
|
||||
// Logger is a state type that accumulates log messages
|
||||
type Logger struct {
|
||||
logs []string
|
||||
}
|
||||
|
||||
// TestApplicativeMonoidBasic tests basic monoid operations
|
||||
func TestApplicativeMonoidBasic(t *testing.T) {
|
||||
t.Run("integer addition", func(t *testing.T) {
|
||||
intAddMonoid := N.MonoidSum[int]()
|
||||
stateMonoid := ApplicativeMonoid[Counter](intAddMonoid)
|
||||
|
||||
s1 := Of[Counter](5)
|
||||
s2 := Of[Counter](3)
|
||||
|
||||
combined := stateMonoid.Concat(s1, s2)
|
||||
result := combined(Counter{count: 10})
|
||||
|
||||
assert.Equal(t, Counter{count: 10}, pair.Head(result))
|
||||
assert.Equal(t, 8, pair.Tail(result))
|
||||
})
|
||||
|
||||
t.Run("integer multiplication", func(t *testing.T) {
|
||||
intMulMonoid := N.MonoidProduct[int]()
|
||||
stateMonoid := ApplicativeMonoid[Counter](intMulMonoid)
|
||||
|
||||
s1 := Of[Counter](4)
|
||||
s2 := Of[Counter](5)
|
||||
|
||||
combined := stateMonoid.Concat(s1, s2)
|
||||
result := combined(Counter{count: 0})
|
||||
|
||||
assert.Equal(t, Counter{count: 0}, pair.Head(result))
|
||||
assert.Equal(t, 20, pair.Tail(result))
|
||||
})
|
||||
|
||||
t.Run("string concatenation", func(t *testing.T) {
|
||||
strMonoid := S.Monoid
|
||||
stateMonoid := ApplicativeMonoid[Counter](strMonoid)
|
||||
|
||||
s1 := Of[Counter]("Hello")
|
||||
s2 := Of[Counter](" World")
|
||||
|
||||
combined := stateMonoid.Concat(s1, s2)
|
||||
result := combined(Counter{count: 5})
|
||||
|
||||
assert.Equal(t, Counter{count: 5}, pair.Head(result))
|
||||
assert.Equal(t, "Hello World", pair.Tail(result))
|
||||
})
|
||||
|
||||
t.Run("boolean AND", func(t *testing.T) {
|
||||
boolAndMonoid := M.MakeMonoid(func(a, b bool) bool { return a && b }, true)
|
||||
stateMonoid := ApplicativeMonoid[Counter](boolAndMonoid)
|
||||
|
||||
s1 := Of[Counter](true)
|
||||
s2 := Of[Counter](true)
|
||||
|
||||
combined := stateMonoid.Concat(s1, s2)
|
||||
result := combined(Counter{count: 0})
|
||||
|
||||
assert.Equal(t, true, pair.Tail(result))
|
||||
|
||||
s3 := Of[Counter](false)
|
||||
combined2 := stateMonoid.Concat(s1, s3)
|
||||
result2 := combined2(Counter{count: 0})
|
||||
|
||||
assert.Equal(t, false, pair.Tail(result2))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMonoidEmpty tests the empty element
|
||||
func TestApplicativeMonoidEmpty(t *testing.T) {
|
||||
t.Run("integer addition empty", func(t *testing.T) {
|
||||
intAddMonoid := N.MonoidSum[int]()
|
||||
stateMonoid := ApplicativeMonoid[Counter](intAddMonoid)
|
||||
|
||||
empty := stateMonoid.Empty()
|
||||
result := empty(Counter{count: 5})
|
||||
|
||||
assert.Equal(t, Counter{count: 5}, pair.Head(result))
|
||||
assert.Equal(t, 0, pair.Tail(result))
|
||||
})
|
||||
|
||||
t.Run("integer multiplication empty", func(t *testing.T) {
|
||||
intMulMonoid := N.MonoidProduct[int]()
|
||||
stateMonoid := ApplicativeMonoid[Counter](intMulMonoid)
|
||||
|
||||
empty := stateMonoid.Empty()
|
||||
result := empty(Counter{count: 10})
|
||||
|
||||
assert.Equal(t, Counter{count: 10}, pair.Head(result))
|
||||
assert.Equal(t, 1, pair.Tail(result))
|
||||
})
|
||||
|
||||
t.Run("string concatenation empty", func(t *testing.T) {
|
||||
strMonoid := S.Monoid
|
||||
stateMonoid := ApplicativeMonoid[Counter](strMonoid)
|
||||
|
||||
empty := stateMonoid.Empty()
|
||||
result := empty(Counter{count: 0})
|
||||
|
||||
assert.Equal(t, Counter{count: 0}, pair.Head(result))
|
||||
assert.Equal(t, "", pair.Tail(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMonoidLaws verifies monoid laws
|
||||
func TestApplicativeMonoidLaws(t *testing.T) {
|
||||
t.Run("associativity with integer addition", func(t *testing.T) {
|
||||
intAddMonoid := N.MonoidSum[int]()
|
||||
m := ApplicativeMonoid[Counter](intAddMonoid)
|
||||
|
||||
s1 := Of[Counter](1)
|
||||
s2 := Of[Counter](2)
|
||||
s3 := Of[Counter](3)
|
||||
|
||||
initialState := Counter{count: 0}
|
||||
|
||||
// (s1 • s2) • s3
|
||||
left := m.Concat(m.Concat(s1, s2), s3)
|
||||
leftResult := left(initialState)
|
||||
|
||||
// s1 • (s2 • s3)
|
||||
right := m.Concat(s1, m.Concat(s2, s3))
|
||||
rightResult := right(initialState)
|
||||
|
||||
assert.Equal(t, leftResult, rightResult)
|
||||
assert.Equal(t, 6, pair.Tail(leftResult))
|
||||
})
|
||||
|
||||
t.Run("left identity with integer addition", func(t *testing.T) {
|
||||
intAddMonoid := N.MonoidSum[int]()
|
||||
m := ApplicativeMonoid[Counter](intAddMonoid)
|
||||
|
||||
s := Of[Counter](42)
|
||||
initialState := Counter{count: 5}
|
||||
|
||||
// Empty() • s = s
|
||||
result := m.Concat(m.Empty(), s)
|
||||
expected := s(initialState)
|
||||
actual := result(initialState)
|
||||
|
||||
assert.Equal(t, expected, actual)
|
||||
assert.Equal(t, 42, pair.Tail(actual))
|
||||
})
|
||||
|
||||
t.Run("right identity with integer addition", func(t *testing.T) {
|
||||
intAddMonoid := N.MonoidSum[int]()
|
||||
m := ApplicativeMonoid[Counter](intAddMonoid)
|
||||
|
||||
s := Of[Counter](42)
|
||||
initialState := Counter{count: 5}
|
||||
|
||||
// s • Empty() = s
|
||||
result := m.Concat(s, m.Empty())
|
||||
expected := s(initialState)
|
||||
actual := result(initialState)
|
||||
|
||||
assert.Equal(t, expected, actual)
|
||||
assert.Equal(t, 42, pair.Tail(actual))
|
||||
})
|
||||
|
||||
t.Run("associativity with string concatenation", func(t *testing.T) {
|
||||
strMonoid := S.Monoid
|
||||
m := ApplicativeMonoid[Counter](strMonoid)
|
||||
|
||||
s1 := Of[Counter]("a")
|
||||
s2 := Of[Counter]("b")
|
||||
s3 := Of[Counter]("c")
|
||||
|
||||
initialState := Counter{count: 0}
|
||||
|
||||
left := m.Concat(m.Concat(s1, s2), s3)
|
||||
leftResult := left(initialState)
|
||||
|
||||
right := m.Concat(s1, m.Concat(s2, s3))
|
||||
rightResult := right(initialState)
|
||||
|
||||
assert.Equal(t, leftResult, rightResult)
|
||||
assert.Equal(t, "abc", pair.Tail(leftResult))
|
||||
})
|
||||
|
||||
t.Run("left identity with string concatenation", func(t *testing.T) {
|
||||
strMonoid := S.Monoid
|
||||
m := ApplicativeMonoid[Counter](strMonoid)
|
||||
|
||||
s := Of[Counter]("test")
|
||||
initialState := Counter{count: 0}
|
||||
|
||||
result := m.Concat(m.Empty(), s)
|
||||
expected := s(initialState)
|
||||
actual := result(initialState)
|
||||
|
||||
assert.Equal(t, expected, actual)
|
||||
assert.Equal(t, "test", pair.Tail(actual))
|
||||
})
|
||||
|
||||
t.Run("right identity with string concatenation", func(t *testing.T) {
|
||||
strMonoid := S.Monoid
|
||||
m := ApplicativeMonoid[Counter](strMonoid)
|
||||
|
||||
s := Of[Counter]("test")
|
||||
initialState := Counter{count: 0}
|
||||
|
||||
result := m.Concat(s, m.Empty())
|
||||
expected := s(initialState)
|
||||
actual := result(initialState)
|
||||
|
||||
assert.Equal(t, expected, actual)
|
||||
assert.Equal(t, "test", pair.Tail(actual))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMonoidWithStateModification tests monoid with state-modifying computations
|
||||
func TestApplicativeMonoidWithStateModification(t *testing.T) {
|
||||
t.Run("state modification with integer addition", func(t *testing.T) {
|
||||
intAddMonoid := N.MonoidSum[int]()
|
||||
stateMonoid := ApplicativeMonoid[Counter](intAddMonoid)
|
||||
|
||||
// Computation that increments counter and returns a value
|
||||
incrementAndReturn := func(val int) State[Counter, int] {
|
||||
return func(s Counter) Pair[Counter, int] {
|
||||
return pair.MakePair(Counter{count: s.count + 1}, val)
|
||||
}
|
||||
}
|
||||
|
||||
s1 := incrementAndReturn(5)
|
||||
s2 := incrementAndReturn(3)
|
||||
|
||||
combined := stateMonoid.Concat(s1, s2)
|
||||
result := combined(Counter{count: 0})
|
||||
|
||||
// State should be incremented twice
|
||||
assert.Equal(t, Counter{count: 2}, pair.Head(result))
|
||||
// Values should be added
|
||||
assert.Equal(t, 8, pair.Tail(result))
|
||||
})
|
||||
|
||||
t.Run("state modification with string concatenation", func(t *testing.T) {
|
||||
strMonoid := S.Monoid
|
||||
stateMonoid := ApplicativeMonoid[Logger](strMonoid)
|
||||
|
||||
// Computation that logs a message and returns it
|
||||
logMessage := func(msg string) State[Logger, string] {
|
||||
return func(s Logger) Pair[Logger, string] {
|
||||
newLogs := append(s.logs, msg)
|
||||
return pair.MakePair(Logger{logs: newLogs}, msg)
|
||||
}
|
||||
}
|
||||
|
||||
s1 := logMessage("Hello")
|
||||
s2 := logMessage(" World")
|
||||
|
||||
combined := stateMonoid.Concat(s1, s2)
|
||||
result := combined(Logger{logs: []string{}})
|
||||
|
||||
// Both messages should be logged
|
||||
assert.Equal(t, []string{"Hello", " World"}, pair.Head(result).logs)
|
||||
// Messages should be concatenated
|
||||
assert.Equal(t, "Hello World", pair.Tail(result))
|
||||
})
|
||||
|
||||
t.Run("complex state transformation", func(t *testing.T) {
|
||||
intAddMonoid := N.MonoidSum[int]()
|
||||
stateMonoid := ApplicativeMonoid[Counter](intAddMonoid)
|
||||
|
||||
// Computation that doubles the counter and returns the old value
|
||||
doubleAndReturnOld := func(val int) State[Counter, int] {
|
||||
return func(s Counter) Pair[Counter, int] {
|
||||
return pair.MakePair(Counter{count: s.count * 2}, val)
|
||||
}
|
||||
}
|
||||
|
||||
s1 := doubleAndReturnOld(10)
|
||||
s2 := doubleAndReturnOld(20)
|
||||
|
||||
combined := stateMonoid.Concat(s1, s2)
|
||||
result := combined(Counter{count: 3})
|
||||
|
||||
// State: 3 -> 6 -> 12
|
||||
assert.Equal(t, Counter{count: 12}, pair.Head(result))
|
||||
// Values: 10 + 20 = 30
|
||||
assert.Equal(t, 30, pair.Tail(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMonoidMultipleConcatenations tests chaining multiple concatenations
|
||||
func TestApplicativeMonoidMultipleConcatenations(t *testing.T) {
|
||||
t.Run("chain of integer additions", func(t *testing.T) {
|
||||
intAddMonoid := N.MonoidSum[int]()
|
||||
m := ApplicativeMonoid[Counter](intAddMonoid)
|
||||
|
||||
states := []State[Counter, int]{
|
||||
Of[Counter](1),
|
||||
Of[Counter](2),
|
||||
Of[Counter](3),
|
||||
Of[Counter](4),
|
||||
Of[Counter](5),
|
||||
}
|
||||
|
||||
// Fold all states using the monoid
|
||||
result := m.Empty()
|
||||
for _, s := range states {
|
||||
result = m.Concat(result, s)
|
||||
}
|
||||
|
||||
finalResult := result(Counter{count: 0})
|
||||
assert.Equal(t, Counter{count: 0}, pair.Head(finalResult))
|
||||
assert.Equal(t, 15, pair.Tail(finalResult)) // 1+2+3+4+5
|
||||
})
|
||||
|
||||
t.Run("chain of string concatenations", func(t *testing.T) {
|
||||
strMonoid := S.Monoid
|
||||
m := ApplicativeMonoid[Counter](strMonoid)
|
||||
|
||||
words := []string{"The", " quick", " brown", " fox"}
|
||||
states := make([]State[Counter, string], len(words))
|
||||
for i, word := range words {
|
||||
states[i] = Of[Counter](word)
|
||||
}
|
||||
|
||||
result := m.Empty()
|
||||
for _, s := range states {
|
||||
result = m.Concat(result, s)
|
||||
}
|
||||
|
||||
finalResult := result(Counter{count: 0})
|
||||
assert.Equal(t, "The quick brown fox", pair.Tail(finalResult))
|
||||
})
|
||||
|
||||
t.Run("nested concatenations", func(t *testing.T) {
|
||||
intAddMonoid := N.MonoidSum[int]()
|
||||
m := ApplicativeMonoid[Counter](intAddMonoid)
|
||||
|
||||
s1 := Of[Counter](1)
|
||||
s2 := Of[Counter](2)
|
||||
s3 := Of[Counter](3)
|
||||
s4 := Of[Counter](4)
|
||||
|
||||
// ((s1 • s2) • s3) • s4
|
||||
result := m.Concat(
|
||||
m.Concat(
|
||||
m.Concat(s1, s2),
|
||||
s3,
|
||||
),
|
||||
s4,
|
||||
)
|
||||
|
||||
finalResult := result(Counter{count: 0})
|
||||
assert.Equal(t, 10, pair.Tail(finalResult))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMonoidEdgeCases tests edge cases
|
||||
func TestApplicativeMonoidEdgeCases(t *testing.T) {
|
||||
t.Run("concatenating empty with empty", func(t *testing.T) {
|
||||
intAddMonoid := N.MonoidSum[int]()
|
||||
m := ApplicativeMonoid[Counter](intAddMonoid)
|
||||
|
||||
result := m.Concat(m.Empty(), m.Empty())
|
||||
finalResult := result(Counter{count: 5})
|
||||
|
||||
assert.Equal(t, Counter{count: 5}, pair.Head(finalResult))
|
||||
assert.Equal(t, 0, pair.Tail(finalResult))
|
||||
})
|
||||
|
||||
t.Run("zero values with multiplication", func(t *testing.T) {
|
||||
intMulMonoid := N.MonoidProduct[int]()
|
||||
m := ApplicativeMonoid[Counter](intMulMonoid)
|
||||
|
||||
s1 := Of[Counter](0)
|
||||
s2 := Of[Counter](42)
|
||||
|
||||
result := m.Concat(s1, s2)
|
||||
finalResult := result(Counter{count: 0})
|
||||
|
||||
assert.Equal(t, 0, pair.Tail(finalResult))
|
||||
})
|
||||
|
||||
t.Run("empty strings", func(t *testing.T) {
|
||||
strMonoid := S.Monoid
|
||||
m := ApplicativeMonoid[Counter](strMonoid)
|
||||
|
||||
s1 := Of[Counter]("")
|
||||
s2 := Of[Counter]("test")
|
||||
|
||||
result := m.Concat(s1, s2)
|
||||
finalResult := result(Counter{count: 0})
|
||||
|
||||
assert.Equal(t, "test", pair.Tail(finalResult))
|
||||
})
|
||||
|
||||
t.Run("negative numbers with addition", func(t *testing.T) {
|
||||
intAddMonoid := N.MonoidSum[int]()
|
||||
m := ApplicativeMonoid[Counter](intAddMonoid)
|
||||
|
||||
s1 := Of[Counter](-5)
|
||||
s2 := Of[Counter](3)
|
||||
|
||||
result := m.Concat(s1, s2)
|
||||
finalResult := result(Counter{count: 0})
|
||||
|
||||
assert.Equal(t, -2, pair.Tail(finalResult))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMonoidWithDifferentTypes tests monoid with various type combinations
|
||||
func TestApplicativeMonoidWithDifferentTypes(t *testing.T) {
|
||||
t.Run("float64 addition", func(t *testing.T) {
|
||||
floatAddMonoid := N.MonoidSum[float64]()
|
||||
m := ApplicativeMonoid[Counter](floatAddMonoid)
|
||||
|
||||
s1 := Of[Counter](1.5)
|
||||
s2 := Of[Counter](2.5)
|
||||
|
||||
result := m.Concat(s1, s2)
|
||||
finalResult := result(Counter{count: 0})
|
||||
|
||||
assert.Equal(t, 4.0, pair.Tail(finalResult))
|
||||
})
|
||||
|
||||
t.Run("float64 multiplication", func(t *testing.T) {
|
||||
floatMulMonoid := N.MonoidProduct[float64]()
|
||||
m := ApplicativeMonoid[Counter](floatMulMonoid)
|
||||
|
||||
s1 := Of[Counter](2.0)
|
||||
s2 := Of[Counter](3.5)
|
||||
|
||||
result := m.Concat(s1, s2)
|
||||
finalResult := result(Counter{count: 0})
|
||||
|
||||
assert.Equal(t, 7.0, pair.Tail(finalResult))
|
||||
})
|
||||
|
||||
t.Run("boolean OR", func(t *testing.T) {
|
||||
boolOrMonoid := M.MakeMonoid(func(a, b bool) bool { return a || b }, false)
|
||||
m := ApplicativeMonoid[Counter](boolOrMonoid)
|
||||
|
||||
s1 := Of[Counter](false)
|
||||
s2 := Of[Counter](true)
|
||||
|
||||
result := m.Concat(s1, s2)
|
||||
finalResult := result(Counter{count: 0})
|
||||
|
||||
assert.Equal(t, true, pair.Tail(finalResult))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMonoidWithGets tests monoid with Gets operations
|
||||
func TestApplicativeMonoidWithGets(t *testing.T) {
|
||||
t.Run("combining state reads", func(t *testing.T) {
|
||||
intAddMonoid := N.MonoidSum[int]()
|
||||
m := ApplicativeMonoid[Counter](intAddMonoid)
|
||||
|
||||
// Read the count from state
|
||||
getCount := Gets(func(c Counter) int { return c.count })
|
||||
|
||||
// Combine two reads
|
||||
combined := m.Concat(getCount, getCount)
|
||||
result := combined(Counter{count: 5})
|
||||
|
||||
// State unchanged
|
||||
assert.Equal(t, Counter{count: 5}, pair.Head(result))
|
||||
// Value is doubled (5 + 5)
|
||||
assert.Equal(t, 10, pair.Tail(result))
|
||||
})
|
||||
|
||||
t.Run("combining different state projections", func(t *testing.T) {
|
||||
intAddMonoid := N.MonoidSum[int]()
|
||||
m := ApplicativeMonoid[Counter](intAddMonoid)
|
||||
|
||||
getCount := Gets(func(c Counter) int { return c.count })
|
||||
getDouble := Gets(func(c Counter) int { return c.count * 2 })
|
||||
|
||||
combined := m.Concat(getCount, getDouble)
|
||||
result := combined(Counter{count: 3})
|
||||
|
||||
assert.Equal(t, Counter{count: 3}, pair.Head(result))
|
||||
assert.Equal(t, 9, pair.Tail(result)) // 3 + 6
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMonoidCommutativity tests behavior with non-commutative operations
|
||||
func TestApplicativeMonoidCommutativity(t *testing.T) {
|
||||
t.Run("string concatenation order matters", func(t *testing.T) {
|
||||
strMonoid := S.Monoid
|
||||
m := ApplicativeMonoid[Counter](strMonoid)
|
||||
|
||||
s1 := Of[Counter]("Hello")
|
||||
s2 := Of[Counter](" World")
|
||||
|
||||
result1 := m.Concat(s1, s2)
|
||||
result2 := m.Concat(s2, s1)
|
||||
|
||||
finalResult1 := result1(Counter{count: 0})
|
||||
finalResult2 := result2(Counter{count: 0})
|
||||
|
||||
assert.Equal(t, "Hello World", pair.Tail(finalResult1))
|
||||
assert.Equal(t, " WorldHello", pair.Tail(finalResult2))
|
||||
assert.NotEqual(t, pair.Tail(finalResult1), pair.Tail(finalResult2))
|
||||
})
|
||||
|
||||
t.Run("subtraction is not commutative", func(t *testing.T) {
|
||||
// Note: Subtraction doesn't form a proper monoid, but we can test it
|
||||
subMonoid := M.MakeMonoid(func(a, b int) int { return a - b }, 0)
|
||||
m := ApplicativeMonoid[Counter](subMonoid)
|
||||
|
||||
s1 := Of[Counter](10)
|
||||
s2 := Of[Counter](3)
|
||||
|
||||
result1 := m.Concat(s1, s2)
|
||||
result2 := m.Concat(s2, s1)
|
||||
|
||||
finalResult1 := result1(Counter{count: 0})
|
||||
finalResult2 := result2(Counter{count: 0})
|
||||
|
||||
assert.Equal(t, 7, pair.Tail(finalResult1)) // 10 - 3
|
||||
assert.Equal(t, -7, pair.Tail(finalResult2)) // 3 - 10
|
||||
assert.NotEqual(t, pair.Tail(finalResult1), pair.Tail(finalResult2))
|
||||
})
|
||||
}
|
||||
79
v2/state/profunctor.go
Normal file
79
v2/state/profunctor.go
Normal file
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package state
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/iso"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// IMap is a profunctor-like operation for State that transforms both the state and value using an isomorphism.
|
||||
// It applies an isomorphism f to the state (contravariantly via Get and covariantly via ReverseGet)
|
||||
// and a function g to the value (covariantly).
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// This operation allows you to:
|
||||
// - Convert the input state from S2 to S1 using the isomorphism's Get function
|
||||
// - Run the State computation with S1
|
||||
// - Convert the output state back from S1 to S2 using the isomorphism's ReverseGet function
|
||||
// - Transform the result value from A to B using g
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The original value type produced by the State
|
||||
// - S2: The new state type
|
||||
// - S1: The original state type expected by the State
|
||||
// - B: The new output value type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: An isomorphism between S2 and S1
|
||||
// - g: Function to transform the output value from A to B
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a State[S1, A] and returns a State[S2, B]
|
||||
//
|
||||
//go:inline
|
||||
func IMap[A, S2, S1, B any](f iso.Iso[S2, S1], g func(A) B) Kleisli[S2, State[S1, A], B] {
|
||||
return F.Bind13of3(F.Flow3[func(s S2) S1, State[S1, A], func(pair.Pair[S1, A]) pair.Pair[S2, B]])(f.Get, pair.BiMap(f.ReverseGet, g))
|
||||
}
|
||||
|
||||
// MapState is a contravariant-like operation for State that transforms the state type using an isomorphism.
|
||||
// It applies an isomorphism f to convert between state types while preserving the value type.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// This operation allows you to:
|
||||
// - Convert the input state from S2 to S1 using the isomorphism's Get function
|
||||
// - Run the State computation with S1
|
||||
// - Convert the output state back from S1 to S2 using the isomorphism's ReverseGet function
|
||||
// - Keep the value type A unchanged
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The value type (unchanged)
|
||||
// - S2: The new state type
|
||||
// - S1: The original state type expected by the State
|
||||
//
|
||||
// Parameters:
|
||||
// - f: An isomorphism between S2 and S1
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a State[S1, A] and returns a State[S2, A]
|
||||
//
|
||||
//go:inline
|
||||
func MapState[A, S2, S1 any](f iso.Iso[S2, S1]) Kleisli[S2, State[S1, A], A] {
|
||||
return F.Bind13of3(F.Flow3[func(S2) S1, State[S1, A], func(pair.Pair[S1, A]) pair.Pair[S2, A]])(f.Get, pair.MapHead[A](f.ReverseGet))
|
||||
}
|
||||
104
v2/state/profunctor_test.go
Normal file
104
v2/state/profunctor_test.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package state
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/optics/iso"
|
||||
P "github.com/IBM/fp-go/v2/pair"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestIMapBasic tests basic IMap functionality
|
||||
func TestIMapBasic(t *testing.T) {
|
||||
t.Run("transform state and value using isomorphism", func(t *testing.T) {
|
||||
// State that increments an int state and returns the old value
|
||||
increment := func(s int) P.Pair[int, int] {
|
||||
return P.MakePair(s+1, s)
|
||||
}
|
||||
|
||||
// Isomorphism between string and int (string length <-> int)
|
||||
stringIntIso := iso.MakeIso(
|
||||
S.Size,
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
// Transform int to string
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := IMap(stringIntIso, toString)(increment)
|
||||
result := adapted("hello") // length is 5
|
||||
|
||||
// State should be "6" (5+1), value should be "5"
|
||||
assert.Equal(t, P.MakePair("6", "5"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMapStateBasic tests basic MapState functionality
|
||||
func TestMapStateBasic(t *testing.T) {
|
||||
t.Run("transform only state using isomorphism", func(t *testing.T) {
|
||||
// State that doubles the state and returns it
|
||||
double := func(s int) P.Pair[int, int] {
|
||||
doubled := s * 2
|
||||
return P.MakePair(doubled, doubled)
|
||||
}
|
||||
|
||||
// Isomorphism between string and int
|
||||
stringIntIso := iso.MakeIso(
|
||||
func(s string) int { n, _ := strconv.Atoi(s); return n },
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
adapted := MapState[int](stringIntIso)(double)
|
||||
result := adapted("5")
|
||||
|
||||
// State should be "10" (5*2), value should be 10
|
||||
assert.Equal(t, P.MakePair("10", 10), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestIMapComposition tests composing IMap transformations
|
||||
func TestIMapComposition(t *testing.T) {
|
||||
t.Run("compose two IMap transformations", func(t *testing.T) {
|
||||
// Simple state that returns the state unchanged
|
||||
identity := func(s int) P.Pair[int, int] {
|
||||
return P.MakePair(s, s)
|
||||
}
|
||||
|
||||
// First isomorphism: bool <-> int (false=0, true=1)
|
||||
boolIntIso := iso.MakeIso(
|
||||
func(b bool) int {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
},
|
||||
func(n int) bool { return n != 0 },
|
||||
)
|
||||
|
||||
// Transform value
|
||||
addOne := func(n int) int { return n + 1 }
|
||||
|
||||
adapted := IMap(boolIntIso, addOne)(identity)
|
||||
result := adapted(true) // true -> 1
|
||||
|
||||
// State should be true (1 -> true), value should be 2 (1+1)
|
||||
assert.Equal(t, P.MakePair(true, 2), result)
|
||||
})
|
||||
}
|
||||
@@ -246,7 +246,7 @@ func TestExecute(t *testing.T) {
|
||||
return TestState{Counter: s.Counter + 1, Message: "new"}
|
||||
})
|
||||
|
||||
finalState := Execute[Void, TestState](initial)(computation)
|
||||
finalState := Execute[Void](initial)(computation)
|
||||
|
||||
assert.Equal(t, 6, finalState.Counter, "counter should be incremented")
|
||||
assert.Equal(t, "new", finalState.Message, "message should be updated")
|
||||
@@ -258,7 +258,7 @@ func TestEvaluate(t *testing.T) {
|
||||
|
||||
computation := Of[TestState](42)
|
||||
|
||||
value := Evaluate[int, TestState](initial)(computation)
|
||||
value := Evaluate[int](initial)(computation)
|
||||
|
||||
assert.Equal(t, 42, value, "value should be 42")
|
||||
}
|
||||
@@ -478,7 +478,7 @@ func TestExecuteWithComplexState(t *testing.T) {
|
||||
|
||||
computation := step2(step1)
|
||||
|
||||
finalState := Execute[Void, TestState](initial)(computation)
|
||||
finalState := Execute[Void](initial)(computation)
|
||||
|
||||
assert.Equal(t, 12, finalState.Counter, "counter should be (1*2)+10 = 12")
|
||||
assert.Equal(t, "end", finalState.Message, "message should be 'end'")
|
||||
@@ -496,7 +496,7 @@ func TestEvaluateWithChain(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
value := Evaluate[string, TestState](initial)(computation)
|
||||
value := Evaluate[string](initial)(computation)
|
||||
|
||||
assert.Equal(t, "result: 20", value, "value should be 'result: 20'")
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@ func BindL[ST, S, T any](
|
||||
// counterLens := lens.Prop[Result, int]("counter")
|
||||
// result := function.Pipe2(
|
||||
// Do[AppState](Result{counter: 5}),
|
||||
// LetL(counterLens, func(n int) int { return n * 2 }),
|
||||
// LetL(counterLens, N.Mul(2)),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
|
||||
152
v2/stateio/monoid.go
Normal file
152
v2/stateio/monoid.go
Normal file
@@ -0,0 +1,152 @@
|
||||
// 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 stateio
|
||||
|
||||
import (
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
)
|
||||
|
||||
// ApplicativeMonoid lifts a monoid into the StateIO applicative functor context.
|
||||
//
|
||||
// This function creates a monoid for StateIO[S, A] values given a monoid for the base type A.
|
||||
// It uses the StateIO monad's applicative operations (Of, MonadMap, MonadAp) to lift the
|
||||
// monoid operations into the StateIO context, allowing you to combine stateful computations
|
||||
// with side effects that produce monoidal values.
|
||||
//
|
||||
// The resulting monoid combines StateIO computations by:
|
||||
// 1. Threading the state through both computations sequentially
|
||||
// 2. Executing the IO effects of both computations in sequence
|
||||
// 3. Combining the produced values using the underlying monoid's Concat operation
|
||||
// 4. Returning a new StateIO computation with the combined value and final state
|
||||
//
|
||||
// The empty element is a StateIO computation that returns the underlying monoid's empty value
|
||||
// without modifying the state or performing any side effects.
|
||||
//
|
||||
// This is particularly useful for:
|
||||
// - Accumulating results across multiple stateful computations with side effects
|
||||
// - Building complex state transformations that aggregate values while performing IO
|
||||
// - Combining independent stateful operations that produce monoidal results
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The state type that is threaded through the computations
|
||||
// - A: The value type that has a monoid structure
|
||||
//
|
||||
// Parameters:
|
||||
// - m: A monoid for the base type A that defines how to combine values
|
||||
//
|
||||
// Returns:
|
||||
// - A Monoid[StateIO[S, A]] that combines stateful IO computations using the base monoid
|
||||
//
|
||||
// The resulting monoid satisfies the standard monoid laws:
|
||||
// - Associativity: Concat(Concat(s1, s2), s3) = Concat(s1, Concat(s2, s3))
|
||||
// - Left identity: Concat(Empty(), s) = s
|
||||
// - Right identity: Concat(s, Empty()) = s
|
||||
//
|
||||
// Example with integer addition:
|
||||
//
|
||||
// import (
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// "github.com/IBM/fp-go/v2/io"
|
||||
// "github.com/IBM/fp-go/v2/pair"
|
||||
// )
|
||||
//
|
||||
// type Counter struct {
|
||||
// count int
|
||||
// }
|
||||
//
|
||||
// // Create a monoid for StateIO[Counter, int] using integer addition
|
||||
// intAddMonoid := N.MonoidSum[int]()
|
||||
// stateIOMonoid := stateio.ApplicativeMonoid[Counter](intAddMonoid)
|
||||
//
|
||||
// // Create two stateful IO computations
|
||||
// s1 := stateio.Of[Counter](5) // Returns 5, state unchanged
|
||||
// s2 := stateio.Of[Counter](3) // Returns 3, state unchanged
|
||||
//
|
||||
// // Combine them using the monoid
|
||||
// combined := stateIOMonoid.Concat(s1, s2)
|
||||
// result := combined(Counter{count: 10})()
|
||||
// // result = Pair{head: Counter{count: 10}, tail: 8} // 5 + 3
|
||||
//
|
||||
// // Empty element
|
||||
// empty := stateIOMonoid.Empty()
|
||||
// emptyResult := empty(Counter{count: 10})()
|
||||
// // emptyResult = Pair{head: Counter{count: 10}, tail: 0}
|
||||
//
|
||||
// Example with string concatenation and state modification:
|
||||
//
|
||||
// import (
|
||||
// S "github.com/IBM/fp-go/v2/string"
|
||||
// "github.com/IBM/fp-go/v2/io"
|
||||
// )
|
||||
//
|
||||
// type Logger struct {
|
||||
// logs []string
|
||||
// }
|
||||
//
|
||||
// strMonoid := S.Monoid
|
||||
// stateIOMonoid := stateio.ApplicativeMonoid[Logger](strMonoid)
|
||||
//
|
||||
// // Stateful IO computation that logs and returns a message
|
||||
// logMessage := func(msg string) stateio.StateIO[Logger, string] {
|
||||
// return func(s Logger) io.IO[pair.Pair[Logger, string]] {
|
||||
// return func() pair.Pair[Logger, string] {
|
||||
// newState := Logger{logs: append(s.logs, msg)}
|
||||
// return pair.MakePair(newState, msg)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// s1 := logMessage("Hello")
|
||||
// s2 := logMessage(" World")
|
||||
//
|
||||
// // Combine the computations - both log entries are added, messages concatenated
|
||||
// combined := stateIOMonoid.Concat(s1, s2)
|
||||
// result := combined(Logger{logs: []string{}})()
|
||||
// // result.head.logs = ["Hello", " World"]
|
||||
// // result.tail = "Hello World"
|
||||
//
|
||||
// Example demonstrating monoid laws:
|
||||
//
|
||||
// intAddMonoid := N.MonoidSum[int]()
|
||||
// m := stateio.ApplicativeMonoid[Counter](intAddMonoid)
|
||||
//
|
||||
// s1 := stateio.Of[Counter](1)
|
||||
// s2 := stateio.Of[Counter](2)
|
||||
// s3 := stateio.Of[Counter](3)
|
||||
//
|
||||
// initialState := Counter{count: 0}
|
||||
//
|
||||
// // Associativity
|
||||
// left := m.Concat(m.Concat(s1, s2), s3)
|
||||
// right := m.Concat(s1, m.Concat(s2, s3))
|
||||
// // Both produce: Pair{head: Counter{count: 0}, tail: 6}
|
||||
//
|
||||
// // Left identity
|
||||
// leftId := m.Concat(m.Empty(), s1)
|
||||
// // Produces: Pair{head: Counter{count: 0}, tail: 1}
|
||||
//
|
||||
// // Right identity
|
||||
// rightId := m.Concat(s1, m.Empty())
|
||||
// // Produces: Pair{head: Counter{count: 0}, tail: 1}
|
||||
//
|
||||
//go:inline
|
||||
func ApplicativeMonoid[S, A any](m M.Monoid[A]) M.Monoid[StateIO[S, A]] {
|
||||
return M.ApplicativeMonoid(
|
||||
Of[S, A],
|
||||
MonadMap[S, A, func(A) A],
|
||||
MonadAp[A, S, A],
|
||||
m)
|
||||
}
|
||||
634
v2/stateio/monoid_test.go
Normal file
634
v2/stateio/monoid_test.go
Normal file
@@ -0,0 +1,634 @@
|
||||
// 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 stateio
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Counter is a simple state type for testing
|
||||
type Counter struct {
|
||||
count int
|
||||
}
|
||||
|
||||
// Logger is a state type that accumulates log messages
|
||||
type Logger struct {
|
||||
logs []string
|
||||
}
|
||||
|
||||
// TestApplicativeMonoidBasic tests basic monoid operations
|
||||
func TestApplicativeMonoidBasic(t *testing.T) {
|
||||
t.Run("integer addition", func(t *testing.T) {
|
||||
intAddMonoid := N.MonoidSum[int]()
|
||||
stateIOMonoid := ApplicativeMonoid[Counter](intAddMonoid)
|
||||
|
||||
s1 := Of[Counter](5)
|
||||
s2 := Of[Counter](3)
|
||||
|
||||
combined := stateIOMonoid.Concat(s1, s2)
|
||||
result := combined(Counter{count: 10})()
|
||||
|
||||
assert.Equal(t, Counter{count: 10}, pair.Head(result))
|
||||
assert.Equal(t, 8, pair.Tail(result))
|
||||
})
|
||||
|
||||
t.Run("integer multiplication", func(t *testing.T) {
|
||||
intMulMonoid := N.MonoidProduct[int]()
|
||||
stateIOMonoid := ApplicativeMonoid[Counter](intMulMonoid)
|
||||
|
||||
s1 := Of[Counter](4)
|
||||
s2 := Of[Counter](5)
|
||||
|
||||
combined := stateIOMonoid.Concat(s1, s2)
|
||||
result := combined(Counter{count: 0})()
|
||||
|
||||
assert.Equal(t, Counter{count: 0}, pair.Head(result))
|
||||
assert.Equal(t, 20, pair.Tail(result))
|
||||
})
|
||||
|
||||
t.Run("string concatenation", func(t *testing.T) {
|
||||
strMonoid := S.Monoid
|
||||
stateIOMonoid := ApplicativeMonoid[Counter](strMonoid)
|
||||
|
||||
s1 := Of[Counter]("Hello")
|
||||
s2 := Of[Counter](" World")
|
||||
|
||||
combined := stateIOMonoid.Concat(s1, s2)
|
||||
result := combined(Counter{count: 5})()
|
||||
|
||||
assert.Equal(t, Counter{count: 5}, pair.Head(result))
|
||||
assert.Equal(t, "Hello World", pair.Tail(result))
|
||||
})
|
||||
|
||||
t.Run("boolean AND", func(t *testing.T) {
|
||||
boolAndMonoid := M.MakeMonoid(func(a, b bool) bool { return a && b }, true)
|
||||
stateIOMonoid := ApplicativeMonoid[Counter](boolAndMonoid)
|
||||
|
||||
s1 := Of[Counter](true)
|
||||
s2 := Of[Counter](true)
|
||||
|
||||
combined := stateIOMonoid.Concat(s1, s2)
|
||||
result := combined(Counter{count: 0})()
|
||||
|
||||
assert.Equal(t, true, pair.Tail(result))
|
||||
|
||||
s3 := Of[Counter](false)
|
||||
combined2 := stateIOMonoid.Concat(s1, s3)
|
||||
result2 := combined2(Counter{count: 0})()
|
||||
|
||||
assert.Equal(t, false, pair.Tail(result2))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMonoidEmpty tests the empty element
|
||||
func TestApplicativeMonoidEmpty(t *testing.T) {
|
||||
t.Run("integer addition empty", func(t *testing.T) {
|
||||
intAddMonoid := N.MonoidSum[int]()
|
||||
stateIOMonoid := ApplicativeMonoid[Counter](intAddMonoid)
|
||||
|
||||
empty := stateIOMonoid.Empty()
|
||||
result := empty(Counter{count: 5})()
|
||||
|
||||
assert.Equal(t, Counter{count: 5}, pair.Head(result))
|
||||
assert.Equal(t, 0, pair.Tail(result))
|
||||
})
|
||||
|
||||
t.Run("integer multiplication empty", func(t *testing.T) {
|
||||
intMulMonoid := N.MonoidProduct[int]()
|
||||
stateIOMonoid := ApplicativeMonoid[Counter](intMulMonoid)
|
||||
|
||||
empty := stateIOMonoid.Empty()
|
||||
result := empty(Counter{count: 10})()
|
||||
|
||||
assert.Equal(t, Counter{count: 10}, pair.Head(result))
|
||||
assert.Equal(t, 1, pair.Tail(result))
|
||||
})
|
||||
|
||||
t.Run("string concatenation empty", func(t *testing.T) {
|
||||
strMonoid := S.Monoid
|
||||
stateIOMonoid := ApplicativeMonoid[Counter](strMonoid)
|
||||
|
||||
empty := stateIOMonoid.Empty()
|
||||
result := empty(Counter{count: 0})()
|
||||
|
||||
assert.Equal(t, Counter{count: 0}, pair.Head(result))
|
||||
assert.Equal(t, "", pair.Tail(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMonoidLaws verifies monoid laws
|
||||
func TestApplicativeMonoidLaws(t *testing.T) {
|
||||
t.Run("associativity with integer addition", func(t *testing.T) {
|
||||
intAddMonoid := N.MonoidSum[int]()
|
||||
m := ApplicativeMonoid[Counter](intAddMonoid)
|
||||
|
||||
s1 := Of[Counter](1)
|
||||
s2 := Of[Counter](2)
|
||||
s3 := Of[Counter](3)
|
||||
|
||||
initialState := Counter{count: 0}
|
||||
|
||||
// (s1 • s2) • s3
|
||||
left := m.Concat(m.Concat(s1, s2), s3)
|
||||
leftResult := left(initialState)()
|
||||
|
||||
// s1 • (s2 • s3)
|
||||
right := m.Concat(s1, m.Concat(s2, s3))
|
||||
rightResult := right(initialState)()
|
||||
|
||||
assert.Equal(t, leftResult, rightResult)
|
||||
assert.Equal(t, 6, pair.Tail(leftResult))
|
||||
})
|
||||
|
||||
t.Run("left identity with integer addition", func(t *testing.T) {
|
||||
intAddMonoid := N.MonoidSum[int]()
|
||||
m := ApplicativeMonoid[Counter](intAddMonoid)
|
||||
|
||||
s := Of[Counter](42)
|
||||
initialState := Counter{count: 5}
|
||||
|
||||
// Empty() • s = s
|
||||
result := m.Concat(m.Empty(), s)
|
||||
expected := s(initialState)()
|
||||
actual := result(initialState)()
|
||||
|
||||
assert.Equal(t, expected, actual)
|
||||
assert.Equal(t, 42, pair.Tail(actual))
|
||||
})
|
||||
|
||||
t.Run("right identity with integer addition", func(t *testing.T) {
|
||||
intAddMonoid := N.MonoidSum[int]()
|
||||
m := ApplicativeMonoid[Counter](intAddMonoid)
|
||||
|
||||
s := Of[Counter](42)
|
||||
initialState := Counter{count: 5}
|
||||
|
||||
// s • Empty() = s
|
||||
result := m.Concat(s, m.Empty())
|
||||
expected := s(initialState)()
|
||||
actual := result(initialState)()
|
||||
|
||||
assert.Equal(t, expected, actual)
|
||||
assert.Equal(t, 42, pair.Tail(actual))
|
||||
})
|
||||
|
||||
t.Run("associativity with string concatenation", func(t *testing.T) {
|
||||
strMonoid := S.Monoid
|
||||
m := ApplicativeMonoid[Counter](strMonoid)
|
||||
|
||||
s1 := Of[Counter]("a")
|
||||
s2 := Of[Counter]("b")
|
||||
s3 := Of[Counter]("c")
|
||||
|
||||
initialState := Counter{count: 0}
|
||||
|
||||
left := m.Concat(m.Concat(s1, s2), s3)
|
||||
leftResult := left(initialState)()
|
||||
|
||||
right := m.Concat(s1, m.Concat(s2, s3))
|
||||
rightResult := right(initialState)()
|
||||
|
||||
assert.Equal(t, leftResult, rightResult)
|
||||
assert.Equal(t, "abc", pair.Tail(leftResult))
|
||||
})
|
||||
|
||||
t.Run("left identity with string concatenation", func(t *testing.T) {
|
||||
strMonoid := S.Monoid
|
||||
m := ApplicativeMonoid[Counter](strMonoid)
|
||||
|
||||
s := Of[Counter]("test")
|
||||
initialState := Counter{count: 0}
|
||||
|
||||
result := m.Concat(m.Empty(), s)
|
||||
expected := s(initialState)()
|
||||
actual := result(initialState)()
|
||||
|
||||
assert.Equal(t, expected, actual)
|
||||
assert.Equal(t, "test", pair.Tail(actual))
|
||||
})
|
||||
|
||||
t.Run("right identity with string concatenation", func(t *testing.T) {
|
||||
strMonoid := S.Monoid
|
||||
m := ApplicativeMonoid[Counter](strMonoid)
|
||||
|
||||
s := Of[Counter]("test")
|
||||
initialState := Counter{count: 0}
|
||||
|
||||
result := m.Concat(s, m.Empty())
|
||||
expected := s(initialState)()
|
||||
actual := result(initialState)()
|
||||
|
||||
assert.Equal(t, expected, actual)
|
||||
assert.Equal(t, "test", pair.Tail(actual))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMonoidWithStateModification tests monoid with state-modifying computations
|
||||
func TestApplicativeMonoidWithStateModification(t *testing.T) {
|
||||
t.Run("state modification with integer addition", func(t *testing.T) {
|
||||
intAddMonoid := N.MonoidSum[int]()
|
||||
stateIOMonoid := ApplicativeMonoid[Counter](intAddMonoid)
|
||||
|
||||
// Computation that increments counter and returns a value
|
||||
incrementAndReturn := func(val int) StateIO[Counter, int] {
|
||||
return func(s Counter) IO[Pair[Counter, int]] {
|
||||
return func() Pair[Counter, int] {
|
||||
return pair.MakePair(Counter{count: s.count + 1}, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s1 := incrementAndReturn(5)
|
||||
s2 := incrementAndReturn(3)
|
||||
|
||||
combined := stateIOMonoid.Concat(s1, s2)
|
||||
result := combined(Counter{count: 0})()
|
||||
|
||||
// State should be incremented twice
|
||||
assert.Equal(t, Counter{count: 2}, pair.Head(result))
|
||||
// Values should be added
|
||||
assert.Equal(t, 8, pair.Tail(result))
|
||||
})
|
||||
|
||||
t.Run("state modification with string concatenation", func(t *testing.T) {
|
||||
strMonoid := S.Monoid
|
||||
stateIOMonoid := ApplicativeMonoid[Logger](strMonoid)
|
||||
|
||||
// Computation that logs a message and returns it
|
||||
logMessage := func(msg string) StateIO[Logger, string] {
|
||||
return func(s Logger) IO[Pair[Logger, string]] {
|
||||
return func() Pair[Logger, string] {
|
||||
newLogs := append(s.logs, msg)
|
||||
return pair.MakePair(Logger{logs: newLogs}, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s1 := logMessage("Hello")
|
||||
s2 := logMessage(" World")
|
||||
|
||||
combined := stateIOMonoid.Concat(s1, s2)
|
||||
result := combined(Logger{logs: []string{}})()
|
||||
|
||||
// Both messages should be logged
|
||||
assert.Equal(t, []string{"Hello", " World"}, pair.Head(result).logs)
|
||||
// Messages should be concatenated
|
||||
assert.Equal(t, "Hello World", pair.Tail(result))
|
||||
})
|
||||
|
||||
t.Run("complex state transformation", func(t *testing.T) {
|
||||
intAddMonoid := N.MonoidSum[int]()
|
||||
stateIOMonoid := ApplicativeMonoid[Counter](intAddMonoid)
|
||||
|
||||
// Computation that doubles the counter and returns the old value
|
||||
doubleAndReturnOld := func(val int) StateIO[Counter, int] {
|
||||
return func(s Counter) IO[Pair[Counter, int]] {
|
||||
return func() Pair[Counter, int] {
|
||||
return pair.MakePair(Counter{count: s.count * 2}, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s1 := doubleAndReturnOld(10)
|
||||
s2 := doubleAndReturnOld(20)
|
||||
|
||||
combined := stateIOMonoid.Concat(s1, s2)
|
||||
result := combined(Counter{count: 3})()
|
||||
|
||||
// State: 3 -> 6 -> 12
|
||||
assert.Equal(t, Counter{count: 12}, pair.Head(result))
|
||||
// Values: 10 + 20 = 30
|
||||
assert.Equal(t, 30, pair.Tail(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMonoidMultipleConcatenations tests chaining multiple concatenations
|
||||
func TestApplicativeMonoidMultipleConcatenations(t *testing.T) {
|
||||
t.Run("chain of integer additions", func(t *testing.T) {
|
||||
intAddMonoid := N.MonoidSum[int]()
|
||||
m := ApplicativeMonoid[Counter](intAddMonoid)
|
||||
|
||||
states := []StateIO[Counter, int]{
|
||||
Of[Counter](1),
|
||||
Of[Counter](2),
|
||||
Of[Counter](3),
|
||||
Of[Counter](4),
|
||||
Of[Counter](5),
|
||||
}
|
||||
|
||||
// Fold all states using the monoid
|
||||
result := m.Empty()
|
||||
for _, s := range states {
|
||||
result = m.Concat(result, s)
|
||||
}
|
||||
|
||||
finalResult := result(Counter{count: 0})()
|
||||
assert.Equal(t, Counter{count: 0}, pair.Head(finalResult))
|
||||
assert.Equal(t, 15, pair.Tail(finalResult)) // 1+2+3+4+5
|
||||
})
|
||||
|
||||
t.Run("chain of string concatenations", func(t *testing.T) {
|
||||
strMonoid := S.Monoid
|
||||
m := ApplicativeMonoid[Counter](strMonoid)
|
||||
|
||||
words := []string{"The", " quick", " brown", " fox"}
|
||||
states := make([]StateIO[Counter, string], len(words))
|
||||
for i, word := range words {
|
||||
states[i] = Of[Counter](word)
|
||||
}
|
||||
|
||||
result := m.Empty()
|
||||
for _, s := range states {
|
||||
result = m.Concat(result, s)
|
||||
}
|
||||
|
||||
finalResult := result(Counter{count: 0})()
|
||||
assert.Equal(t, "The quick brown fox", pair.Tail(finalResult))
|
||||
})
|
||||
|
||||
t.Run("nested concatenations", func(t *testing.T) {
|
||||
intAddMonoid := N.MonoidSum[int]()
|
||||
m := ApplicativeMonoid[Counter](intAddMonoid)
|
||||
|
||||
s1 := Of[Counter](1)
|
||||
s2 := Of[Counter](2)
|
||||
s3 := Of[Counter](3)
|
||||
s4 := Of[Counter](4)
|
||||
|
||||
// ((s1 • s2) • s3) • s4
|
||||
result := m.Concat(
|
||||
m.Concat(
|
||||
m.Concat(s1, s2),
|
||||
s3,
|
||||
),
|
||||
s4,
|
||||
)
|
||||
|
||||
finalResult := result(Counter{count: 0})()
|
||||
assert.Equal(t, 10, pair.Tail(finalResult))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMonoidEdgeCases tests edge cases
|
||||
func TestApplicativeMonoidEdgeCases(t *testing.T) {
|
||||
t.Run("concatenating empty with empty", func(t *testing.T) {
|
||||
intAddMonoid := N.MonoidSum[int]()
|
||||
m := ApplicativeMonoid[Counter](intAddMonoid)
|
||||
|
||||
result := m.Concat(m.Empty(), m.Empty())
|
||||
finalResult := result(Counter{count: 5})()
|
||||
|
||||
assert.Equal(t, Counter{count: 5}, pair.Head(finalResult))
|
||||
assert.Equal(t, 0, pair.Tail(finalResult))
|
||||
})
|
||||
|
||||
t.Run("zero values with multiplication", func(t *testing.T) {
|
||||
intMulMonoid := N.MonoidProduct[int]()
|
||||
m := ApplicativeMonoid[Counter](intMulMonoid)
|
||||
|
||||
s1 := Of[Counter](0)
|
||||
s2 := Of[Counter](42)
|
||||
|
||||
result := m.Concat(s1, s2)
|
||||
finalResult := result(Counter{count: 0})()
|
||||
|
||||
assert.Equal(t, 0, pair.Tail(finalResult))
|
||||
})
|
||||
|
||||
t.Run("empty strings", func(t *testing.T) {
|
||||
strMonoid := S.Monoid
|
||||
m := ApplicativeMonoid[Counter](strMonoid)
|
||||
|
||||
s1 := Of[Counter]("")
|
||||
s2 := Of[Counter]("test")
|
||||
|
||||
result := m.Concat(s1, s2)
|
||||
finalResult := result(Counter{count: 0})()
|
||||
|
||||
assert.Equal(t, "test", pair.Tail(finalResult))
|
||||
})
|
||||
|
||||
t.Run("negative numbers with addition", func(t *testing.T) {
|
||||
intAddMonoid := N.MonoidSum[int]()
|
||||
m := ApplicativeMonoid[Counter](intAddMonoid)
|
||||
|
||||
s1 := Of[Counter](-5)
|
||||
s2 := Of[Counter](3)
|
||||
|
||||
result := m.Concat(s1, s2)
|
||||
finalResult := result(Counter{count: 0})()
|
||||
|
||||
assert.Equal(t, -2, pair.Tail(finalResult))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMonoidWithDifferentTypes tests monoid with various type combinations
|
||||
func TestApplicativeMonoidWithDifferentTypes(t *testing.T) {
|
||||
t.Run("float64 addition", func(t *testing.T) {
|
||||
floatAddMonoid := N.MonoidSum[float64]()
|
||||
m := ApplicativeMonoid[Counter](floatAddMonoid)
|
||||
|
||||
s1 := Of[Counter](1.5)
|
||||
s2 := Of[Counter](2.5)
|
||||
|
||||
result := m.Concat(s1, s2)
|
||||
finalResult := result(Counter{count: 0})()
|
||||
|
||||
assert.Equal(t, 4.0, pair.Tail(finalResult))
|
||||
})
|
||||
|
||||
t.Run("float64 multiplication", func(t *testing.T) {
|
||||
floatMulMonoid := N.MonoidProduct[float64]()
|
||||
m := ApplicativeMonoid[Counter](floatMulMonoid)
|
||||
|
||||
s1 := Of[Counter](2.0)
|
||||
s2 := Of[Counter](3.5)
|
||||
|
||||
result := m.Concat(s1, s2)
|
||||
finalResult := result(Counter{count: 0})()
|
||||
|
||||
assert.Equal(t, 7.0, pair.Tail(finalResult))
|
||||
})
|
||||
|
||||
t.Run("boolean OR", func(t *testing.T) {
|
||||
boolOrMonoid := M.MakeMonoid(func(a, b bool) bool { return a || b }, false)
|
||||
m := ApplicativeMonoid[Counter](boolOrMonoid)
|
||||
|
||||
s1 := Of[Counter](false)
|
||||
s2 := Of[Counter](true)
|
||||
|
||||
result := m.Concat(s1, s2)
|
||||
finalResult := result(Counter{count: 0})()
|
||||
|
||||
assert.Equal(t, true, pair.Tail(finalResult))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMonoidWithIO tests monoid with actual IO effects
|
||||
func TestApplicativeMonoidWithIO(t *testing.T) {
|
||||
t.Run("combining IO effects", func(t *testing.T) {
|
||||
intAddMonoid := N.MonoidSum[int]()
|
||||
m := ApplicativeMonoid[Counter](intAddMonoid)
|
||||
|
||||
// StateIO that performs IO and modifies state
|
||||
effectfulComputation := func(val int, increment int) StateIO[Counter, int] {
|
||||
return func(s Counter) IO[Pair[Counter, int]] {
|
||||
return func() Pair[Counter, int] {
|
||||
// Simulate IO effect
|
||||
newCount := s.count + increment
|
||||
return pair.MakePair(Counter{count: newCount}, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s1 := effectfulComputation(10, 1)
|
||||
s2 := effectfulComputation(20, 2)
|
||||
|
||||
combined := m.Concat(s1, s2)
|
||||
result := combined(Counter{count: 0})()
|
||||
|
||||
// State should be incremented by 1 then by 2
|
||||
assert.Equal(t, Counter{count: 3}, pair.Head(result))
|
||||
// Values should be added
|
||||
assert.Equal(t, 30, pair.Tail(result))
|
||||
})
|
||||
|
||||
t.Run("IO effects with logging", func(t *testing.T) {
|
||||
strMonoid := S.Monoid
|
||||
m := ApplicativeMonoid[Logger](strMonoid)
|
||||
|
||||
// StateIO that logs and returns a message
|
||||
logAndReturn := func(msg string) StateIO[Logger, string] {
|
||||
return func(s Logger) IO[Pair[Logger, string]] {
|
||||
return func() Pair[Logger, string] {
|
||||
// Simulate IO logging effect
|
||||
newLogs := append(append([]string{}, s.logs...), msg)
|
||||
return pair.MakePair(Logger{logs: newLogs}, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s1 := logAndReturn("First")
|
||||
s2 := logAndReturn("Second")
|
||||
s3 := logAndReturn("Third")
|
||||
|
||||
combined := m.Concat(m.Concat(s1, s2), s3)
|
||||
result := combined(Logger{logs: []string{}})()
|
||||
|
||||
assert.Equal(t, []string{"First", "Second", "Third"}, pair.Head(result).logs)
|
||||
assert.Equal(t, "FirstSecondThird", pair.Tail(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMonoidCommutativity tests behavior with non-commutative operations
|
||||
func TestApplicativeMonoidCommutativity(t *testing.T) {
|
||||
t.Run("string concatenation order matters", func(t *testing.T) {
|
||||
strMonoid := S.Monoid
|
||||
m := ApplicativeMonoid[Counter](strMonoid)
|
||||
|
||||
s1 := Of[Counter]("Hello")
|
||||
s2 := Of[Counter](" World")
|
||||
|
||||
result1 := m.Concat(s1, s2)
|
||||
result2 := m.Concat(s2, s1)
|
||||
|
||||
finalResult1 := result1(Counter{count: 0})()
|
||||
finalResult2 := result2(Counter{count: 0})()
|
||||
|
||||
assert.Equal(t, "Hello World", pair.Tail(finalResult1))
|
||||
assert.Equal(t, " WorldHello", pair.Tail(finalResult2))
|
||||
assert.NotEqual(t, pair.Tail(finalResult1), pair.Tail(finalResult2))
|
||||
})
|
||||
|
||||
t.Run("subtraction is not commutative", func(t *testing.T) {
|
||||
// Note: Subtraction doesn't form a proper monoid, but we can test it
|
||||
subMonoid := M.MakeMonoid(func(a, b int) int { return a - b }, 0)
|
||||
m := ApplicativeMonoid[Counter](subMonoid)
|
||||
|
||||
s1 := Of[Counter](10)
|
||||
s2 := Of[Counter](3)
|
||||
|
||||
result1 := m.Concat(s1, s2)
|
||||
result2 := m.Concat(s2, s1)
|
||||
|
||||
finalResult1 := result1(Counter{count: 0})()
|
||||
finalResult2 := result2(Counter{count: 0})()
|
||||
|
||||
assert.Equal(t, 7, pair.Tail(finalResult1)) // 10 - 3
|
||||
assert.Equal(t, -7, pair.Tail(finalResult2)) // 3 - 10
|
||||
assert.NotEqual(t, pair.Tail(finalResult1), pair.Tail(finalResult2))
|
||||
})
|
||||
|
||||
t.Run("state modification order matters", func(t *testing.T) {
|
||||
intAddMonoid := N.MonoidSum[int]()
|
||||
m := ApplicativeMonoid[Counter](intAddMonoid)
|
||||
|
||||
// Computation that uses current state value
|
||||
useState := func(multiplier int) StateIO[Counter, int] {
|
||||
return func(s Counter) IO[Pair[Counter, int]] {
|
||||
return func() Pair[Counter, int] {
|
||||
result := s.count * multiplier
|
||||
newState := Counter{count: s.count + 1}
|
||||
return pair.MakePair(newState, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s1 := useState(2)
|
||||
s2 := useState(3)
|
||||
|
||||
// Order matters because state is threaded through
|
||||
result1 := m.Concat(s1, s2)
|
||||
result2 := m.Concat(s2, s1)
|
||||
|
||||
finalResult1 := result1(Counter{count: 5})()
|
||||
finalResult2 := result2(Counter{count: 5})()
|
||||
|
||||
// s1 then s2: (5*2) + ((5+1)*3) = 10 + 18 = 28
|
||||
assert.Equal(t, 28, pair.Tail(finalResult1))
|
||||
// s2 then s1: (5*3) + ((5+1)*2) = 15 + 12 = 27
|
||||
assert.Equal(t, 27, pair.Tail(finalResult2))
|
||||
assert.NotEqual(t, pair.Tail(finalResult1), pair.Tail(finalResult2))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMonoidWithFromIO tests monoid with FromIO
|
||||
func TestApplicativeMonoidWithFromIO(t *testing.T) {
|
||||
t.Run("combining FromIO computations", func(t *testing.T) {
|
||||
intAddMonoid := N.MonoidSum[int]()
|
||||
m := ApplicativeMonoid[Counter](intAddMonoid)
|
||||
|
||||
// Create StateIO from IO
|
||||
io1 := io.Of(5)
|
||||
io2 := io.Of(3)
|
||||
|
||||
s1 := FromIO[Counter](io1)
|
||||
s2 := FromIO[Counter](io2)
|
||||
|
||||
combined := m.Concat(s1, s2)
|
||||
result := combined(Counter{count: 10})()
|
||||
|
||||
assert.Equal(t, Counter{count: 10}, pair.Head(result))
|
||||
assert.Equal(t, 8, pair.Tail(result))
|
||||
})
|
||||
}
|
||||
@@ -13,6 +13,9 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package generic provides generic string utility functions that work with any type
|
||||
// that has string as its underlying type (using the ~string constraint).
|
||||
// This allows these functions to work with custom string types while maintaining type safety.
|
||||
package generic
|
||||
|
||||
// ToBytes converts the string to bytes
|
||||
|
||||
164
v2/string/generic/string_test.go
Normal file
164
v2/string/generic/string_test.go
Normal file
@@ -0,0 +1,164 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package generic
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Custom string type for testing generic constraints
|
||||
type MyString string
|
||||
|
||||
func TestToBytes(t *testing.T) {
|
||||
t.Run("regular string", func(t *testing.T) {
|
||||
result := ToBytes("hello")
|
||||
expected := []byte{'h', 'e', 'l', 'l', 'o'}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("empty string", func(t *testing.T) {
|
||||
result := ToBytes("")
|
||||
assert.Equal(t, []byte{}, result)
|
||||
})
|
||||
|
||||
t.Run("custom string type", func(t *testing.T) {
|
||||
result := ToBytes(MyString("test"))
|
||||
expected := []byte{'t', 'e', 's', 't'}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("unicode string", func(t *testing.T) {
|
||||
result := ToBytes("你好")
|
||||
// UTF-8 encoding: 你 = E4 BD A0, 好 = E5 A5 BD
|
||||
assert.Equal(t, 6, len(result))
|
||||
})
|
||||
}
|
||||
|
||||
func TestToRunes(t *testing.T) {
|
||||
t.Run("regular string", func(t *testing.T) {
|
||||
result := ToRunes("hello")
|
||||
expected := []rune{'h', 'e', 'l', 'l', 'o'}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("empty string", func(t *testing.T) {
|
||||
result := ToRunes("")
|
||||
assert.Equal(t, []rune{}, result)
|
||||
})
|
||||
|
||||
t.Run("custom string type", func(t *testing.T) {
|
||||
result := ToRunes(MyString("test"))
|
||||
expected := []rune{'t', 'e', 's', 't'}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("unicode string", func(t *testing.T) {
|
||||
result := ToRunes("你好")
|
||||
assert.Equal(t, 2, len(result))
|
||||
assert.Equal(t, '你', result[0])
|
||||
assert.Equal(t, '好', result[1])
|
||||
})
|
||||
|
||||
t.Run("mixed ascii and unicode", func(t *testing.T) {
|
||||
result := ToRunes("Hello世界")
|
||||
assert.Equal(t, 7, len(result))
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsEmpty(t *testing.T) {
|
||||
t.Run("empty string", func(t *testing.T) {
|
||||
assert.True(t, IsEmpty(""))
|
||||
})
|
||||
|
||||
t.Run("non-empty string", func(t *testing.T) {
|
||||
assert.False(t, IsEmpty("hello"))
|
||||
})
|
||||
|
||||
t.Run("whitespace string", func(t *testing.T) {
|
||||
assert.False(t, IsEmpty(" "))
|
||||
assert.False(t, IsEmpty("\t"))
|
||||
assert.False(t, IsEmpty("\n"))
|
||||
})
|
||||
|
||||
t.Run("custom string type empty", func(t *testing.T) {
|
||||
assert.True(t, IsEmpty(MyString("")))
|
||||
})
|
||||
|
||||
t.Run("custom string type non-empty", func(t *testing.T) {
|
||||
assert.False(t, IsEmpty(MyString("test")))
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsNonEmpty(t *testing.T) {
|
||||
t.Run("empty string", func(t *testing.T) {
|
||||
assert.False(t, IsNonEmpty(""))
|
||||
})
|
||||
|
||||
t.Run("non-empty string", func(t *testing.T) {
|
||||
assert.True(t, IsNonEmpty("hello"))
|
||||
})
|
||||
|
||||
t.Run("whitespace string", func(t *testing.T) {
|
||||
assert.True(t, IsNonEmpty(" "))
|
||||
assert.True(t, IsNonEmpty("\t"))
|
||||
assert.True(t, IsNonEmpty("\n"))
|
||||
})
|
||||
|
||||
t.Run("custom string type empty", func(t *testing.T) {
|
||||
assert.False(t, IsNonEmpty(MyString("")))
|
||||
})
|
||||
|
||||
t.Run("custom string type non-empty", func(t *testing.T) {
|
||||
assert.True(t, IsNonEmpty(MyString("test")))
|
||||
})
|
||||
|
||||
t.Run("single character", func(t *testing.T) {
|
||||
assert.True(t, IsNonEmpty("a"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestSize(t *testing.T) {
|
||||
t.Run("empty string", func(t *testing.T) {
|
||||
assert.Equal(t, 0, Size(""))
|
||||
})
|
||||
|
||||
t.Run("ascii string", func(t *testing.T) {
|
||||
assert.Equal(t, 5, Size("hello"))
|
||||
assert.Equal(t, 11, Size("hello world"))
|
||||
})
|
||||
|
||||
t.Run("custom string type", func(t *testing.T) {
|
||||
assert.Equal(t, 4, Size(MyString("test")))
|
||||
})
|
||||
|
||||
t.Run("unicode string - returns byte count", func(t *testing.T) {
|
||||
// Note: Size returns byte length, not rune count
|
||||
assert.Equal(t, 6, Size("你好")) // 2 Chinese characters = 6 bytes in UTF-8
|
||||
assert.Equal(t, 5, Size("café")) // 'c', 'a', 'f' = 3 bytes, 'é' = 2 bytes in UTF-8
|
||||
})
|
||||
|
||||
t.Run("single character", func(t *testing.T) {
|
||||
assert.Equal(t, 1, Size("a"))
|
||||
})
|
||||
|
||||
t.Run("whitespace", func(t *testing.T) {
|
||||
assert.Equal(t, 1, Size(" "))
|
||||
assert.Equal(t, 1, Size("\t"))
|
||||
assert.Equal(t, 1, Size("\n"))
|
||||
})
|
||||
}
|
||||
@@ -19,12 +19,46 @@ import (
|
||||
"testing"
|
||||
|
||||
M "github.com/IBM/fp-go/v2/monoid/testing"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMonoid(t *testing.T) {
|
||||
M.AssertLaws(t, Monoid)([]string{"", "a", "some value"})
|
||||
}
|
||||
|
||||
func TestMonoidConcat(t *testing.T) {
|
||||
t.Run("basic concatenation", func(t *testing.T) {
|
||||
result := Monoid.Concat("hello", " world")
|
||||
assert.Equal(t, "hello world", result)
|
||||
})
|
||||
|
||||
t.Run("empty identity", func(t *testing.T) {
|
||||
empty := Monoid.Empty()
|
||||
assert.Equal(t, "", empty)
|
||||
})
|
||||
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
// empty • a = a
|
||||
result := Monoid.Concat(Monoid.Empty(), "test")
|
||||
assert.Equal(t, "test", result)
|
||||
})
|
||||
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
// a • empty = a
|
||||
result := Monoid.Concat("test", Monoid.Empty())
|
||||
assert.Equal(t, "test", result)
|
||||
})
|
||||
|
||||
t.Run("associativity", func(t *testing.T) {
|
||||
// (a • b) • c = a • (b • c)
|
||||
a, b, c := "foo", "bar", "baz"
|
||||
left := Monoid.Concat(Monoid.Concat(a, b), c)
|
||||
right := Monoid.Concat(a, Monoid.Concat(b, c))
|
||||
assert.Equal(t, left, right)
|
||||
assert.Equal(t, "foobarbaz", left)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntersperseMonoid(t *testing.T) {
|
||||
// Test with comma separator
|
||||
commaMonoid := IntersperseMonoid(", ")
|
||||
@@ -34,3 +68,51 @@ func TestIntersperseMonoid(t *testing.T) {
|
||||
dashMonoid := IntersperseMonoid("-")
|
||||
M.AssertLaws(t, dashMonoid)([]string{"", "x", "y", "test"})
|
||||
}
|
||||
|
||||
func TestIntersperseMonoidConcat(t *testing.T) {
|
||||
t.Run("comma separator", func(t *testing.T) {
|
||||
commaMonoid := IntersperseMonoid(", ")
|
||||
result := commaMonoid.Concat("a", "b")
|
||||
assert.Equal(t, "a, b", result)
|
||||
})
|
||||
|
||||
t.Run("empty identity", func(t *testing.T) {
|
||||
commaMonoid := IntersperseMonoid(", ")
|
||||
empty := commaMonoid.Empty()
|
||||
assert.Equal(t, "", empty)
|
||||
})
|
||||
|
||||
t.Run("left identity with separator", func(t *testing.T) {
|
||||
// empty • a = a (no separator added)
|
||||
commaMonoid := IntersperseMonoid(", ")
|
||||
result := commaMonoid.Concat(commaMonoid.Empty(), "test")
|
||||
assert.Equal(t, "test", result)
|
||||
})
|
||||
|
||||
t.Run("right identity with separator", func(t *testing.T) {
|
||||
// a • empty = a (no separator added)
|
||||
commaMonoid := IntersperseMonoid(", ")
|
||||
result := commaMonoid.Concat("test", commaMonoid.Empty())
|
||||
assert.Equal(t, "test", result)
|
||||
})
|
||||
|
||||
t.Run("associativity with separator", func(t *testing.T) {
|
||||
// (a • b) • c = a • (b • c)
|
||||
commaMonoid := IntersperseMonoid(", ")
|
||||
a, b, c := "x", "y", "z"
|
||||
left := commaMonoid.Concat(commaMonoid.Concat(a, b), c)
|
||||
right := commaMonoid.Concat(a, commaMonoid.Concat(b, c))
|
||||
assert.Equal(t, left, right)
|
||||
assert.Equal(t, "x, y, z", left)
|
||||
})
|
||||
|
||||
t.Run("multiple separators", func(t *testing.T) {
|
||||
dashMonoid := IntersperseMonoid("-")
|
||||
result := dashMonoid.Concat("foo", "bar")
|
||||
assert.Equal(t, "foo-bar", result)
|
||||
|
||||
spaceMonoid := IntersperseMonoid(" ")
|
||||
result = spaceMonoid.Concat("hello", "world")
|
||||
assert.Equal(t, "hello world", result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,14 +16,13 @@
|
||||
package string
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
)
|
||||
|
||||
// concat concatenates two strings
|
||||
// concat concatenates two strings using simple string concatenation.
|
||||
// This is an internal helper function used by the Semigroup and Monoid implementations.
|
||||
func concat(left, right string) string {
|
||||
return fmt.Sprintf("%s%s", left, right)
|
||||
return left + right
|
||||
}
|
||||
|
||||
// Semigroup is the semigroup implementing string concatenation
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package string provides functional programming utilities for working with strings.
|
||||
// It includes functions for string manipulation, comparison, conversion, and formatting,
|
||||
// following functional programming principles with curried functions and composable operations.
|
||||
package string
|
||||
|
||||
import (
|
||||
@@ -42,7 +45,7 @@ var (
|
||||
// Includes returns a predicate that tests for the existence of the search string
|
||||
Includes = F.Bind2of2(strings.Contains)
|
||||
|
||||
// HasPrefix returns a predicate that checks if the prefis is included in the string
|
||||
// HasPrefix returns a predicate that checks if the prefix is included in the string
|
||||
HasPrefix = F.Bind2of2(strings.HasPrefix)
|
||||
)
|
||||
|
||||
@@ -103,3 +106,31 @@ func Intersperse(middle string) func(string, string) string {
|
||||
return l + middle + r
|
||||
}
|
||||
}
|
||||
|
||||
// Prepend returns a function that prepends a prefix to a string.
|
||||
// This is a curried function that takes a prefix and returns a function
|
||||
// that prepends that prefix to any string passed to it.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// addHello := Prepend("Hello, ")
|
||||
// result := addHello("World") // "Hello, World"
|
||||
func Prepend(prefix string) func(string) string {
|
||||
return func(suffix string) string {
|
||||
return prefix + suffix
|
||||
}
|
||||
}
|
||||
|
||||
// Append returns a function that appends a suffix to a string.
|
||||
// This is a curried function that takes a suffix and returns a function
|
||||
// that appends that suffix to any string passed to it.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// addExclamation := Append("!")
|
||||
// result := addExclamation("Hello") // "Hello!"
|
||||
func Append(suffix string) func(string) string {
|
||||
return func(prefix string) string {
|
||||
return prefix + suffix
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,3 +141,99 @@ func TestOrd(t *testing.T) {
|
||||
assert.True(t, Ord.Compare("a", "a") == 0)
|
||||
assert.True(t, Ord.Compare("abc", "abd") < 0)
|
||||
}
|
||||
|
||||
func TestPrepend(t *testing.T) {
|
||||
t.Run("prepend to non-empty string", func(t *testing.T) {
|
||||
addHello := Prepend("Hello, ")
|
||||
result := addHello("World")
|
||||
assert.Equal(t, "Hello, World", result)
|
||||
})
|
||||
|
||||
t.Run("prepend to empty string", func(t *testing.T) {
|
||||
addPrefix := Prepend("prefix")
|
||||
result := addPrefix("")
|
||||
assert.Equal(t, "prefix", result)
|
||||
})
|
||||
|
||||
t.Run("prepend empty string", func(t *testing.T) {
|
||||
addNothing := Prepend("")
|
||||
result := addNothing("test")
|
||||
assert.Equal(t, "test", result)
|
||||
})
|
||||
|
||||
t.Run("prepend with special characters", func(t *testing.T) {
|
||||
addSymbols := Prepend(">>> ")
|
||||
result := addSymbols("message")
|
||||
assert.Equal(t, ">>> message", result)
|
||||
})
|
||||
|
||||
t.Run("prepend with unicode", func(t *testing.T) {
|
||||
addEmoji := Prepend("🎉 ")
|
||||
result := addEmoji("Party!")
|
||||
assert.Equal(t, "🎉 Party!", result)
|
||||
})
|
||||
|
||||
t.Run("multiple prepends", func(t *testing.T) {
|
||||
addA := Prepend("A")
|
||||
addB := Prepend("B")
|
||||
result := addB(addA("C"))
|
||||
assert.Equal(t, "BAC", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppend(t *testing.T) {
|
||||
t.Run("append to non-empty string", func(t *testing.T) {
|
||||
addExclamation := Append("!")
|
||||
result := addExclamation("Hello")
|
||||
assert.Equal(t, "Hello!", result)
|
||||
})
|
||||
|
||||
t.Run("append to empty string", func(t *testing.T) {
|
||||
addSuffix := Append("suffix")
|
||||
result := addSuffix("")
|
||||
assert.Equal(t, "suffix", result)
|
||||
})
|
||||
|
||||
t.Run("append empty string", func(t *testing.T) {
|
||||
addNothing := Append("")
|
||||
result := addNothing("test")
|
||||
assert.Equal(t, "test", result)
|
||||
})
|
||||
|
||||
t.Run("append with special characters", func(t *testing.T) {
|
||||
addEllipsis := Append("...")
|
||||
result := addEllipsis("To be continued")
|
||||
assert.Equal(t, "To be continued...", result)
|
||||
})
|
||||
|
||||
t.Run("append with unicode", func(t *testing.T) {
|
||||
addEmoji := Append(" 🎉")
|
||||
result := addEmoji("Party")
|
||||
assert.Equal(t, "Party 🎉", result)
|
||||
})
|
||||
|
||||
t.Run("multiple appends", func(t *testing.T) {
|
||||
addA := Append("A")
|
||||
addB := Append("B")
|
||||
result := addB(addA("C"))
|
||||
assert.Equal(t, "CAB", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPrependAndAppend(t *testing.T) {
|
||||
t.Run("combine prepend and append", func(t *testing.T) {
|
||||
addPrefix := Prepend("[ ")
|
||||
addSuffix := Append(" ]")
|
||||
result := addSuffix(addPrefix("content"))
|
||||
assert.Equal(t, "[ content ]", result)
|
||||
})
|
||||
|
||||
t.Run("chain multiple operations", func(t *testing.T) {
|
||||
addQuotes := Prepend("\"")
|
||||
closeQuotes := Append("\"")
|
||||
addLabel := Prepend("Value: ")
|
||||
|
||||
result := addLabel(addQuotes(closeQuotes("test")))
|
||||
assert.Equal(t, "Value: \"test\"", result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -62,14 +62,14 @@
|
||||
// t := tuple.MakeTuple2(5, "hello")
|
||||
// mapper := tuple.Map2(
|
||||
// func(n int) string { return fmt.Sprintf("%d", n) },
|
||||
// func(s string) int { return len(s) },
|
||||
// S.Size,
|
||||
// )
|
||||
// result := mapper(t) // Tuple2[string, int]{"5", 5}
|
||||
//
|
||||
// BiMap transforms both elements of a Tuple2:
|
||||
//
|
||||
// mapper := tuple.BiMap(
|
||||
// func(s string) int { return len(s) },
|
||||
// S.Size,
|
||||
// func(n int) string { return fmt.Sprintf("%d", n*2) },
|
||||
// )
|
||||
//
|
||||
|
||||
@@ -18,6 +18,7 @@ package tuple_test
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/IBM/fp-go/v2/tuple"
|
||||
)
|
||||
|
||||
@@ -63,7 +64,7 @@ func ExampleOf2() {
|
||||
func ExampleBiMap() {
|
||||
t := tuple.MakeTuple2(5, "hello")
|
||||
mapper := tuple.BiMap(
|
||||
func(s string) int { return len(s) },
|
||||
S.Size,
|
||||
func(n int) string { return fmt.Sprintf("%d", n*2) },
|
||||
)
|
||||
result := mapper(t)
|
||||
|
||||
@@ -95,7 +95,7 @@ func Of2[T1, T2 any](e T2) func(T1) Tuple2[T1, T2] {
|
||||
//
|
||||
// t := tuple.MakeTuple2(5, "hello")
|
||||
// mapper := tuple.BiMap(
|
||||
// func(s string) int { return len(s) },
|
||||
// S.Size,
|
||||
// func(n int) string { return fmt.Sprintf("%d", n*2) },
|
||||
// )
|
||||
// result := mapper(t) // Returns Tuple2[string, int]{F1: "10", F2: 5}
|
||||
|
||||
@@ -89,7 +89,7 @@ func TestOf2(t *testing.T) {
|
||||
func TestBiMap(t *testing.T) {
|
||||
t2 := MakeTuple2(5, "hello")
|
||||
mapper := BiMap(
|
||||
func(s string) int { return len(s) },
|
||||
S.Size,
|
||||
func(n int) string { return fmt.Sprintf("%d", n*2) },
|
||||
)
|
||||
result := mapper(t2)
|
||||
@@ -135,7 +135,7 @@ func TestMap2(t *testing.T) {
|
||||
t2 := MakeTuple2(5, "hello")
|
||||
mapper := Map2(
|
||||
func(n int) string { return fmt.Sprintf("%d", n*2) },
|
||||
func(s string) int { return len(s) },
|
||||
S.Size,
|
||||
)
|
||||
result := mapper(t2)
|
||||
assert.Equal(t, MakeTuple2("10", 5), result)
|
||||
|
||||
Reference in New Issue
Block a user