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

Compare commits

..

6 Commits
v2.1.4 ... main

Author SHA1 Message Date
Dr. Carsten Leue
c6445ac021 fix: better tests and docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-14 12:09:01 +01:00
Dr. Carsten Leue
840ffbb51d fix: documentation and missing tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-13 20:27:46 +01:00
Dr. Carsten Leue
380ba2853c fix: update profunctor docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-13 16:15:37 +01:00
Dr. Carsten Leue
c18e5e2107 fix: add monoid to state
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-12 23:03:57 +01:00
Dr. Carsten Leue
89766bdb26 fix: add monoid to state
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-12 23:03:37 +01:00
Dr. Carsten Leue
21d116d325 fix: implement monoid for Pair
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-12 22:53:19 +01:00
93 changed files with 9447 additions and 158 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,298 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package generic
import (
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
// TestExtract tests the Extract function
func TestExtract(t *testing.T) {
t.Run("Extract from non-empty array", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5}
result := Extract(input)
assert.Equal(t, 1, result)
})
t.Run("Extract from single element array", func(t *testing.T) {
input := []string{"hello"}
result := Extract(input)
assert.Equal(t, "hello", result)
})
t.Run("Extract from empty array returns zero value", func(t *testing.T) {
input := []int{}
result := Extract(input)
assert.Equal(t, 0, result)
})
t.Run("Extract from empty string array returns empty string", func(t *testing.T) {
input := []string{}
result := Extract(input)
assert.Equal(t, "", result)
})
t.Run("Extract does not modify original array", func(t *testing.T) {
original := []int{1, 2, 3}
originalCopy := []int{1, 2, 3}
_ = Extract(original)
assert.Equal(t, originalCopy, original)
})
t.Run("Extract with floats", func(t *testing.T) {
input := []float64{3.14, 2.71, 1.41}
result := Extract(input)
assert.Equal(t, 3.14, result)
})
t.Run("Extract with custom slice type", func(t *testing.T) {
type IntSlice []int
input := IntSlice{10, 20, 30}
result := Extract(input)
assert.Equal(t, 10, result)
})
}
// TestExtractComonadLaws tests comonad laws for Extract
func TestExtractComonadLaws(t *testing.T) {
t.Run("Extract ∘ Of == Identity", func(t *testing.T) {
value := 42
result := Extract(Of[[]int](value))
assert.Equal(t, value, result)
})
t.Run("Extract ∘ Extend(f) == f", func(t *testing.T) {
input := []int{1, 2, 3, 4}
f := func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
}
// Extract(Extend(f)(input)) should equal f(input)
extended := Extend[[]int, []int](f)(input)
result := Extract(extended)
expected := f(input)
assert.Equal(t, expected, result)
})
}
// TestExtend tests the Extend function
func TestExtend(t *testing.T) {
t.Run("Extend with sum of suffixes", func(t *testing.T) {
input := []int{1, 2, 3, 4}
sumSuffix := Extend[[]int, []int](func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
})
result := sumSuffix(input)
expected := []int{10, 9, 7, 4} // [1+2+3+4, 2+3+4, 3+4, 4]
assert.Equal(t, expected, result)
})
t.Run("Extend with length of suffixes", func(t *testing.T) {
input := []int{10, 20, 30}
lengths := Extend[[]int, []int](Size[[]int, int])
result := lengths(input)
expected := []int{3, 2, 1}
assert.Equal(t, expected, result)
})
t.Run("Extend with head extraction", func(t *testing.T) {
input := []int{1, 2, 3}
duplicate := Extend[[]int, []int](Extract[[]int, int])
result := duplicate(input)
expected := []int{1, 2, 3}
assert.Equal(t, expected, result)
})
t.Run("Extend with empty array", func(t *testing.T) {
input := []int{}
result := Extend[[]int, []int](Size[[]int, int])(input)
assert.Equal(t, []int{}, result)
})
t.Run("Extend with single element", func(t *testing.T) {
input := []string{"hello"}
result := Extend[[]string, []int](func(as []string) int { return len(as) })(input)
expected := []int{1}
assert.Equal(t, expected, result)
})
t.Run("Extend does not modify original array", func(t *testing.T) {
original := []int{1, 2, 3}
originalCopy := []int{1, 2, 3}
_ = Extend[[]int, []int](Size[[]int, int])(original)
assert.Equal(t, originalCopy, original)
})
t.Run("Extend with string concatenation", func(t *testing.T) {
input := []string{"a", "b", "c"}
concat := Extend[[]string, []string](func(as []string) string {
return MonadReduce(as, func(acc, s string) string { return acc + s }, "")
})
result := concat(input)
expected := []string{"abc", "bc", "c"}
assert.Equal(t, expected, result)
})
t.Run("Extend with custom slice types", func(t *testing.T) {
type IntSlice []int
type ResultSlice []int
input := IntSlice{1, 2, 3}
sumSuffix := Extend[IntSlice, ResultSlice](func(as IntSlice) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
})
result := sumSuffix(input)
expected := ResultSlice{6, 5, 3}
assert.Equal(t, expected, result)
})
}
// TestExtendComonadLaws tests comonad laws for Extend
func TestExtendComonadLaws(t *testing.T) {
t.Run("Left identity: Extend(Extract) == Identity", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5}
result := Extend[[]int, []int](Extract[[]int, int])(input)
assert.Equal(t, input, result)
})
t.Run("Right identity: Extract ∘ Extend(f) == f", func(t *testing.T) {
input := []int{1, 2, 3, 4}
f := func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
}
// Extract(Extend(f)(input)) should equal f(input)
result := F.Pipe2(input, Extend[[]int, []int](f), Extract[[]int, int])
expected := f(input)
assert.Equal(t, expected, result)
})
t.Run("Associativity: Extend(f) ∘ Extend(g) == Extend(f ∘ Extend(g))", func(t *testing.T) {
input := []int{1, 2, 3}
// f: sum of array
f := func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
}
// g: length of array
g := func(as []int) int {
return len(as)
}
// Left side: Extend(f) ∘ Extend(g)
left := F.Pipe2(input, Extend[[]int, []int](g), Extend[[]int, []int](f))
// Right side: Extend(f ∘ Extend(g))
right := Extend[[]int, []int](func(as []int) int {
return f(Extend[[]int, []int](g)(as))
})(input)
assert.Equal(t, left, right)
})
}
// TestExtendComposition tests Extend with other array operations
func TestExtendComposition(t *testing.T) {
t.Run("Extend after Map", func(t *testing.T) {
input := []int{1, 2, 3}
result := F.Pipe2(
input,
Map[[]int, []int](func(x int) int { return x * 2 }),
Extend[[]int, []int](func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
}),
)
expected := []int{12, 10, 6} // [2+4+6, 4+6, 6]
assert.Equal(t, expected, result)
})
t.Run("Map after Extend", func(t *testing.T) {
input := []int{1, 2, 3}
result := F.Pipe2(
input,
Extend[[]int, []int](Size[[]int, int]),
Map[[]int, []int](func(x int) int { return x * 10 }),
)
expected := []int{30, 20, 10}
assert.Equal(t, expected, result)
})
t.Run("Extend with Filter", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5, 6}
result := F.Pipe2(
input,
Filter[[]int](func(n int) bool { return n%2 == 0 }),
Extend[[]int, []int](Size[[]int, int]),
)
expected := []int{3, 2, 1} // lengths of [2,4,6], [4,6], [6]
assert.Equal(t, expected, result)
})
}
// TestExtendUseCases demonstrates practical use cases for Extend
func TestExtendUseCases(t *testing.T) {
t.Run("Running sum (cumulative sum from each position)", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5}
runningSum := Extend[[]int, []int](func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
})
result := runningSum(input)
expected := []int{15, 14, 12, 9, 5}
assert.Equal(t, expected, result)
})
t.Run("Sliding window average", func(t *testing.T) {
input := []float64{1.0, 2.0, 3.0, 4.0, 5.0}
windowAvg := Extend[[]float64, []float64](func(as []float64) float64 {
if len(as) == 0 {
return 0
}
sum := MonadReduce(as, func(acc, x float64) float64 { return acc + x }, 0.0)
return sum / float64(len(as))
})
result := windowAvg(input)
expected := []float64{3.0, 3.5, 4.0, 4.5, 5.0}
assert.Equal(t, expected, result)
})
t.Run("Check if suffix is sorted", func(t *testing.T) {
input := []int{1, 2, 3, 2, 1}
isSorted := Extend[[]int, []bool](func(as []int) bool {
for i := 1; i < len(as); i++ {
if as[i] < as[i-1] {
return false
}
}
return true
})
result := isSorted(input)
expected := []bool{false, false, false, false, true}
assert.Equal(t, expected, result)
})
t.Run("Count remaining elements", func(t *testing.T) {
events := []string{"start", "middle", "end"}
remaining := Extend[[]string, []int](Size[[]string, string])
result := remaining(events)
expected := []int{3, 2, 1}
assert.Equal(t, expected, result)
})
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,74 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerio
import (
"context"
"github.com/IBM/fp-go/v2/function"
)
// Promap is the profunctor map operation that transforms both the input and output of a context-based ReaderIO.
// It applies f to the input context (contravariantly) and g to the output value (covariantly).
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// This operation allows you to:
// - Modify the context before passing it to the ReaderIO (via f)
// - Transform the result value after the IO effect completes (via g)
//
// The function f returns both a new context and a CancelFunc that should be called to release resources.
//
// Type Parameters:
// - A: The original result type produced by the ReaderIO
// - B: The new output result type
//
// Parameters:
// - f: Function to transform the input context (contravariant)
// - g: Function to transform the output value from A to B (covariant)
//
// Returns:
// - An Operator that takes a ReaderIO[A] and returns a ReaderIO[B]
//
//go:inline
func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFunc), g func(A) B) Operator[A, B] {
return function.Flow2(
Local[A](f),
Map(g),
)
}
// Contramap changes the context during the execution of a ReaderIO.
// This is the contravariant functor operation that transforms the input context.
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// Contramap is an alias for Local and is useful for adapting a ReaderIO to work with
// a modified context by providing a function that transforms the context.
//
// Type Parameters:
// - A: The result type (unchanged)
//
// Parameters:
// - f: Function to transform the context, returning a new context and CancelFunc
//
// Returns:
// - An Operator that takes a ReaderIO[A] and returns a ReaderIO[A]
//
//go:inline
func Contramap[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
return Local[A](f)
}

View File

@@ -0,0 +1,97 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerio
import (
"context"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// TestPromapBasic tests basic Promap functionality
func TestPromapBasic(t *testing.T) {
t.Run("transform both context and output", func(t *testing.T) {
// ReaderIO that reads a value from context
getValue := func(ctx context.Context) IO[int] {
return func() int {
if v := ctx.Value("key"); v != nil {
return v.(int)
}
return 0
}
}
// Transform context and result
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
newCtx := context.WithValue(ctx, "key", 42)
return newCtx, func() {}
}
toString := strconv.Itoa
adapted := Promap(addKey, toString)(getValue)
result := adapted(context.Background())()
assert.Equal(t, "42", result)
})
}
// TestContramapBasic tests basic Contramap functionality
func TestContramapBasic(t *testing.T) {
t.Run("context transformation", func(t *testing.T) {
getValue := func(ctx context.Context) IO[int] {
return func() int {
if v := ctx.Value("key"); v != nil {
return v.(int)
}
return 0
}
}
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
newCtx := context.WithValue(ctx, "key", 100)
return newCtx, func() {}
}
adapted := Contramap[int](addKey)(getValue)
result := adapted(context.Background())()
assert.Equal(t, 100, result)
})
}
// TestLocalBasic tests basic Local functionality
func TestLocalBasic(t *testing.T) {
t.Run("adds timeout to context", func(t *testing.T) {
getValue := func(ctx context.Context) IO[bool] {
return func() bool {
_, hasDeadline := ctx.Deadline()
return hasDeadline
}
}
addTimeout := func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, time.Second)
}
adapted := Local[bool](addTimeout)(getValue)
result := adapted(context.Background())()
assert.True(t, result)
})
}

View File

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

View File

@@ -0,0 +1,75 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerioresult
import (
"context"
"github.com/IBM/fp-go/v2/function"
)
// Promap is the profunctor map operation that transforms both the input and output of a context-based ReaderIOResult.
// It applies f to the input context (contravariantly) and g to the output value (covariantly).
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// This operation allows you to:
// - Modify the context before passing it to the ReaderIOResult (via f)
// - Transform the success value after the IO effect completes (via g)
//
// The function f returns both a new context and a CancelFunc that should be called to release resources.
// The error type is fixed as error and remains unchanged through the transformation.
//
// Type Parameters:
// - A: The original success type produced by the ReaderIOResult
// - B: The new output success type
//
// Parameters:
// - f: Function to transform the input context (contravariant)
// - g: Function to transform the output success value from A to B (covariant)
//
// Returns:
// - An Operator that takes a ReaderIOResult[A] and returns a ReaderIOResult[B]
//
//go:inline
func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFunc), g func(A) B) Operator[A, B] {
return function.Flow2(
Local[A](f),
Map(g),
)
}
// Contramap changes the context during the execution of a ReaderIOResult.
// This is the contravariant functor operation that transforms the input context.
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// Contramap is an alias for Local and is useful for adapting a ReaderIOResult to work with
// a modified context by providing a function that transforms the context.
//
// Type Parameters:
// - A: The success type (unchanged)
//
// Parameters:
// - f: Function to transform the context, returning a new context and CancelFunc
//
// Returns:
// - An Operator that takes a ReaderIOResult[A] and returns a ReaderIOResult[A]
//
//go:inline
func Contramap[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
return Local[A](f)
}

View File

@@ -0,0 +1,98 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerioresult
import (
"context"
"strconv"
"testing"
R "github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
// TestPromapBasic tests basic Promap functionality
func TestPromapBasic(t *testing.T) {
t.Run("transform both context and output", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[int] {
return func() R.Result[int] {
if v := ctx.Value("key"); v != nil {
return R.Of(v.(int))
}
return R.Of(0)
}
}
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
newCtx := context.WithValue(ctx, "key", 42)
return newCtx, func() {}
}
toString := strconv.Itoa
adapted := Promap(addKey, toString)(getValue)
result := adapted(context.Background())()
assert.Equal(t, R.Of("42"), result)
})
}
// TestContramapBasic tests basic Contramap functionality
func TestContramapBasic(t *testing.T) {
t.Run("context transformation", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[int] {
return func() R.Result[int] {
if v := ctx.Value("key"); v != nil {
return R.Of(v.(int))
}
return R.Of(0)
}
}
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
newCtx := context.WithValue(ctx, "key", 100)
return newCtx, func() {}
}
adapted := Contramap[int](addKey)(getValue)
result := adapted(context.Background())()
assert.Equal(t, R.Of(100), result)
})
}
// TestLocalBasic tests basic Local functionality
func TestLocalBasic(t *testing.T) {
t.Run("adds value to context", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
if v := ctx.Value("user"); v != nil {
return R.Of(v.(string))
}
return R.Of("unknown")
}
}
addUser := func(ctx context.Context) (context.Context, context.CancelFunc) {
newCtx := context.WithValue(ctx, "user", "Alice")
return newCtx, func() {}
}
adapted := Local[string](addUser)(getValue)
result := adapted(context.Background())()
assert.Equal(t, R.Of("Alice"), result)
})
}

View File

@@ -0,0 +1,106 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerresult
import (
"context"
"github.com/IBM/fp-go/v2/function"
)
// Promap is the profunctor map operation that transforms both the input and output of a context-based ReaderResult.
// It applies f to the input context (contravariantly) and g to the output value (covariantly).
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// This operation allows you to:
// - Modify the context before passing it to the ReaderResult (via f)
// - Transform the success value after the computation completes (via g)
//
// The function f returns both a new context and a CancelFunc that should be called to release resources.
// The error type is fixed as error and remains unchanged through the transformation.
//
// Type Parameters:
// - A: The original success type produced by the ReaderResult
// - B: The new output success type
//
// Parameters:
// - f: Function to transform the input context (contravariant)
// - g: Function to transform the output success value from A to B (covariant)
//
// Returns:
// - An Operator that takes a ReaderResult[A] and returns a ReaderResult[B]
//
//go:inline
func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFunc), g func(A) B) Operator[A, B] {
return function.Flow2(
Local[A](f),
Map(g),
)
}
// Contramap changes the context during the execution of a ReaderResult.
// This is the contravariant functor operation that transforms the input context.
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// Contramap is an alias for Local and is useful for adapting a ReaderResult to work with
// a modified context by providing a function that transforms the context.
//
// Type Parameters:
// - A: The success type (unchanged)
//
// Parameters:
// - f: Function to transform the context, returning a new context and CancelFunc
//
// Returns:
// - An Operator that takes a ReaderResult[A] and returns a ReaderResult[A]
//
//go:inline
func Contramap[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
return Local[A](f)
}
// Local changes the context during the execution of a ReaderResult.
// This allows you to modify the context before passing it to a ReaderResult computation.
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// Local is particularly useful for:
// - Adding values to the context
// - Setting timeouts or deadlines
// - Modifying context metadata
//
// The function f returns both a new context and a CancelFunc. The CancelFunc is automatically
// called (via defer) after the ReaderResult computation completes to ensure proper cleanup.
//
// Type Parameters:
// - A: The result type (unchanged)
//
// Parameters:
// - f: Function to transform the context, returning a new context and CancelFunc
//
// Returns:
// - An Operator that takes a ReaderResult[A] and returns a ReaderResult[A]
func Local[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
return func(rr ReaderResult[A]) ReaderResult[A] {
return func(ctx context.Context) Result[A] {
otherCtx, otherCancel := f(ctx)
defer otherCancel()
return rr(otherCtx)
}
}
}

View File

@@ -0,0 +1,92 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerresult
import (
"context"
"strconv"
"testing"
R "github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
// TestPromapBasic tests basic Promap functionality
func TestPromapBasic(t *testing.T) {
t.Run("transform both context and output", func(t *testing.T) {
getValue := func(ctx context.Context) Result[int] {
if v := ctx.Value("key"); v != nil {
return R.Of(v.(int))
}
return R.Of(0)
}
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
newCtx := context.WithValue(ctx, "key", 42)
return newCtx, func() {}
}
toString := strconv.Itoa
adapted := Promap(addKey, toString)(getValue)
result := adapted(context.Background())
assert.Equal(t, R.Of("42"), result)
})
}
// TestContramapBasic tests basic Contramap functionality
func TestContramapBasic(t *testing.T) {
t.Run("context transformation", func(t *testing.T) {
getValue := func(ctx context.Context) Result[int] {
if v := ctx.Value("key"); v != nil {
return R.Of(v.(int))
}
return R.Of(0)
}
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
newCtx := context.WithValue(ctx, "key", 100)
return newCtx, func() {}
}
adapted := Contramap[int](addKey)(getValue)
result := adapted(context.Background())
assert.Equal(t, R.Of(100), result)
})
}
// TestLocalBasic tests basic Local functionality
func TestLocalBasic(t *testing.T) {
t.Run("adds value to context", func(t *testing.T) {
getValue := func(ctx context.Context) Result[string] {
if v := ctx.Value("user"); v != nil {
return R.Of(v.(string))
}
return R.Of("unknown")
}
addUser := func(ctx context.Context) (context.Context, context.CancelFunc) {
newCtx := context.WithValue(ctx, "user", "Alice")
return newCtx, func() {}
}
adapted := Local[string](addUser)(getValue)
result := adapted(context.Background())
assert.Equal(t, R.Of("Alice"), result)
})
}

View File

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

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

@@ -0,0 +1,91 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package either
import F "github.com/IBM/fp-go/v2/function"
// MonadExtend applies a function to an Either value, where the function receives the entire Either as input.
// This is the Extend (or Comonad) operation that allows computations to depend on the context.
//
// If the Either is Left, it returns Left unchanged without applying the function.
// If the Either is Right, it applies the function to the entire Either and wraps the result in a Right.
//
// This operation is useful when you need to perform computations that depend on whether
// a value is present (Right) or absent (Left), not just on the value itself.
//
// Type Parameters:
// - E: The error type (Left channel)
// - A: The input value type (Right channel)
// - B: The output value type
//
// Parameters:
// - fa: The Either value to extend
// - f: Function that takes the entire Either[E, A] and produces a value of type B
//
// Returns:
// - Either[E, B]: Left if input was Left, otherwise Right containing the result of f(fa)
//
// Example:
//
// // Count how many times we've seen a Right value
// counter := func(e either.Either[error, int]) int {
// return either.Fold(
// func(err error) int { return 0 },
// func(n int) int { return 1 },
// )(e)
// }
// result := either.MonadExtend(either.Right[error](42), counter) // Right(1)
// result := either.MonadExtend(either.Left[int](errors.New("err")), counter) // Left(error)
//
//go:inline
func MonadExtend[E, A, B any](fa Either[E, A], f func(Either[E, A]) B) Either[E, B] {
if fa.isLeft {
return Left[B](fa.l)
}
return Of[E](f(fa))
}
// Extend is the curried version of [MonadExtend].
// It returns a function that applies the given function to an Either value.
//
// This is useful for creating reusable transformations that depend on the Either context.
//
// Type Parameters:
// - E: The error type (Left channel)
// - A: The input value type (Right channel)
// - B: The output value type
//
// Parameters:
// - f: Function that takes the entire Either[E, A] and produces a value of type B
//
// Returns:
// - Operator[E, A, B]: A function that transforms Either[E, A] to Either[E, B]
//
// Example:
//
// // Create a reusable extender that extracts metadata
// getMetadata := either.Extend(func(e either.Either[error, string]) string {
// return either.Fold(
// func(err error) string { return "error: " + err.Error() },
// func(s string) string { return "value: " + s },
// )(e)
// })
// result := getMetadata(either.Right[error]("hello")) // Right("value: hello")
//
//go:inline
func Extend[E, A, B any](f func(Either[E, A]) B) Operator[E, A, B] {
return F.Bind2nd(MonadExtend[E, A, B], f)
}

View File

@@ -0,0 +1,377 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package either
import (
"errors"
"strconv"
"testing"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
// TestMonadExtendWithRight tests MonadExtend with Right values
func TestMonadExtendWithRight(t *testing.T) {
t.Run("applies function to Right value", func(t *testing.T) {
input := Right[error](42)
// Function that extracts and doubles the value if Right
f := func(e Either[error, int]) int {
return Fold(
F.Constant1[error](0),
N.Mul(2),
)(e)
}
result := MonadExtend(input, f)
assert.True(t, IsRight(result))
assert.Equal(t, 84, GetOrElse(F.Constant1[error](0))(result))
})
t.Run("function receives entire Either context", func(t *testing.T) {
input := Right[error]("hello")
// Function that creates metadata about the Either
f := func(e Either[error, string]) string {
return Fold(
func(err error) string { return "error: " + err.Error() },
S.Prepend("value: "),
)(e)
}
result := MonadExtend(input, f)
assert.True(t, IsRight(result))
assert.Equal(t, "value: hello", GetOrElse(func(error) string { return "" })(result))
})
t.Run("can count Right occurrences", func(t *testing.T) {
input := Right[error](100)
counter := func(e Either[error, int]) int {
return Fold(
F.Constant1[error](0),
F.Constant1[int](1),
)(e)
}
result := MonadExtend(input, counter)
assert.True(t, IsRight(result))
assert.Equal(t, 1, GetOrElse(func(error) int { return -1 })(result))
})
}
// TestMonadExtendWithLeft tests MonadExtend with Left values
func TestMonadExtendWithLeft(t *testing.T) {
t.Run("returns Left without applying function", func(t *testing.T) {
testErr := errors.New("test error")
input := Left[int](testErr)
// Function should not be called
called := false
f := func(e Either[error, int]) int {
called = true
return 42
}
result := MonadExtend(input, f)
assert.False(t, called, "function should not be called for Left")
assert.True(t, IsLeft(result))
_, leftVal := Unwrap(result)
assert.Equal(t, testErr, leftVal)
})
t.Run("preserves Left error type", func(t *testing.T) {
input := Left[string](errors.New("original error"))
f := func(e Either[error, string]) string {
return "should not be called"
}
result := MonadExtend(input, f)
assert.True(t, IsLeft(result))
_, leftVal := Unwrap(result)
assert.Equal(t, "original error", leftVal.Error())
})
}
// TestMonadExtendEdgeCases tests edge cases for MonadExtend
func TestMonadExtendEdgeCases(t *testing.T) {
t.Run("function returns zero value", func(t *testing.T) {
input := Right[error](42)
f := func(e Either[error, int]) int {
return 0
}
result := MonadExtend(input, f)
assert.True(t, IsRight(result))
assert.Equal(t, 0, GetOrElse(func(error) int { return -1 })(result))
})
t.Run("function changes type", func(t *testing.T) {
input := Right[error](42)
f := func(e Either[error, int]) string {
return Fold(
F.Constant1[error]("error"),
S.Format[int]("number: %d"),
)(e)
}
result := MonadExtend(input, f)
assert.True(t, IsRight(result))
assert.Equal(t, "number: 42", GetOrElse(func(error) string { return "" })(result))
})
t.Run("nested Either handling", func(t *testing.T) {
inner := Right[error](10)
outer := Right[error](inner)
// Extract the inner value
f := func(e Either[error, Either[error, int]]) int {
return Fold(
F.Constant1[error](-1),
func(innerEither Either[error, int]) int {
return GetOrElse(F.Constant1[error](-2))(innerEither)
},
)(e)
}
result := MonadExtend(outer, f)
assert.True(t, IsRight(result))
assert.Equal(t, 10, GetOrElse(F.Constant1[error](-3))(result))
})
}
// TestExtendWithRight tests Extend (curried version) with Right values
func TestExtendWithRight(t *testing.T) {
t.Run("creates reusable extender", func(t *testing.T) {
// Create a reusable extender
doubler := Extend(func(e Either[error, int]) int {
return Fold(
F.Constant1[error](0),
N.Mul(2),
)(e)
})
result1 := doubler(Right[error](21))
result2 := doubler(Right[error](50))
assert.True(t, IsRight(result1))
assert.Equal(t, 42, GetOrElse(F.Constant1[error](0))(result1))
assert.True(t, IsRight(result2))
assert.Equal(t, 100, GetOrElse(F.Constant1[error](0))(result2))
})
t.Run("metadata extractor", func(t *testing.T) {
getMetadata := Extend(func(e Either[error, string]) string {
return Fold(
func(err error) string { return "error: " + err.Error() },
S.Prepend("value: "),
)(e)
})
result := getMetadata(Right[error]("test"))
assert.True(t, IsRight(result))
assert.Equal(t, "value: test", GetOrElse(func(error) string { return "" })(result))
})
t.Run("composition with other operations", func(t *testing.T) {
// Create an extender that counts characters
charCounter := Extend(func(e Either[error, string]) int {
return Fold(
F.Constant1[error](0),
S.Size,
)(e)
})
// Apply to a Right value
input := Right[error]("hello")
result := charCounter(input)
assert.True(t, IsRight(result))
assert.Equal(t, 5, GetOrElse(func(error) int { return -1 })(result))
})
}
// TestExtendWithLeft tests Extend with Left values
func TestExtendWithLeft(t *testing.T) {
t.Run("returns Left without calling function", func(t *testing.T) {
testErr := errors.New("test error")
called := false
extender := Extend(func(e Either[error, int]) int {
called = true
return 42
})
result := extender(Left[int](testErr))
assert.False(t, called, "function should not be called for Left")
assert.True(t, IsLeft(result))
_, leftVal := Unwrap(result)
assert.Equal(t, testErr, leftVal)
})
t.Run("preserves error through multiple applications", func(t *testing.T) {
originalErr := errors.New("original")
extender := Extend(func(e Either[error, string]) string {
return "transformed"
})
result := extender(Left[string](originalErr))
assert.True(t, IsLeft(result))
_, leftVal := Unwrap(result)
assert.Equal(t, originalErr, leftVal)
})
}
// TestExtendChaining tests chaining multiple Extend operations
func TestExtendChaining(t *testing.T) {
t.Run("chain multiple extenders", func(t *testing.T) {
// First extender: double the value
doubler := Extend(func(e Either[error, int]) int {
return Fold(
F.Constant1[error](0),
N.Mul(2),
)(e)
})
// Second extender: add 10
adder := Extend(func(e Either[error, int]) int {
return Fold(
F.Constant1[error](0),
N.Add(10),
)(e)
})
input := Right[error](5)
result := adder(doubler(input))
assert.True(t, IsRight(result))
assert.Equal(t, 20, GetOrElse(F.Constant1[error](0))(result))
})
t.Run("short-circuits on Left", func(t *testing.T) {
testErr := errors.New("error")
extender1 := Extend(func(e Either[error, int]) int { return 1 })
extender2 := Extend(func(e Either[error, int]) int { return 2 })
input := Left[int](testErr)
result := extender2(extender1(input))
assert.True(t, IsLeft(result))
_, leftVal := Unwrap(result)
assert.Equal(t, testErr, leftVal)
})
}
// TestExtendTypeTransformations tests type transformations with Extend
func TestExtendTypeTransformations(t *testing.T) {
t.Run("int to string transformation", func(t *testing.T) {
toString := Extend(func(e Either[error, int]) string {
return Fold(
F.Constant1[error]("error"),
strconv.Itoa,
)(e)
})
result := toString(Right[error](42))
assert.True(t, IsRight(result))
assert.Equal(t, "42", GetOrElse(func(error) string { return "" })(result))
})
t.Run("string to bool transformation", func(t *testing.T) {
isEmpty := Extend(func(e Either[error, string]) bool {
return Fold(
func(err error) bool { return true },
func(s string) bool { return len(s) == 0 },
)(e)
})
result1 := isEmpty(Right[error](""))
result2 := isEmpty(Right[error]("hello"))
assert.True(t, IsRight(result1))
assert.True(t, GetOrElse(func(error) bool { return false })(result1))
assert.True(t, IsRight(result2))
assert.False(t, GetOrElse(func(error) bool { return true })(result2))
})
}
// TestExtendWithComplexTypes tests Extend with complex types
func TestExtendWithComplexTypes(t *testing.T) {
type User struct {
Name string
Age int
}
t.Run("extract field from struct", func(t *testing.T) {
getName := Extend(func(e Either[error, User]) string {
return Fold(
func(err error) string { return "unknown" },
func(u User) string { return u.Name },
)(e)
})
user := User{Name: "Alice", Age: 30}
result := getName(Right[error](user))
assert.True(t, IsRight(result))
assert.Equal(t, "Alice", GetOrElse(func(error) string { return "" })(result))
})
t.Run("compute derived value", func(t *testing.T) {
isAdult := Extend(func(e Either[error, User]) bool {
return Fold(
func(err error) bool { return false },
func(u User) bool { return u.Age >= 18 },
)(e)
})
user1 := User{Name: "Bob", Age: 25}
user2 := User{Name: "Charlie", Age: 15}
result1 := isAdult(Right[error](user1))
result2 := isAdult(Right[error](user2))
assert.True(t, IsRight(result1))
assert.True(t, GetOrElse(func(error) bool { return false })(result1))
assert.True(t, IsRight(result2))
assert.False(t, GetOrElse(func(error) bool { return true })(result2))
})
}
// Made with Bob

View File

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

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

@@ -0,0 +1,89 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package file provides functional programming utilities for working with file paths
// and I/O interfaces in Go.
//
// # Overview
//
// This package offers a collection of utility functions designed to work seamlessly
// with functional programming patterns, particularly with the fp-go library's pipe
// and composition utilities.
//
// # Path Manipulation
//
// The Join function provides a curried approach to path joining, making it easy to
// create reusable path builders:
//
// import (
// F "github.com/IBM/fp-go/v2/function"
// "github.com/IBM/fp-go/v2/file"
// )
//
// // Create a reusable path builder
// addConfig := file.Join("config.json")
// configPath := addConfig("/etc/myapp")
// // Result: "/etc/myapp/config.json"
//
// // Use in a functional pipeline
// logPath := F.Pipe1("/var/log", file.Join("app.log"))
// // Result: "/var/log/app.log"
//
// // Chain multiple joins
// deepPath := F.Pipe2(
// "/root",
// file.Join("subdir"),
// file.Join("file.txt"),
// )
// // Result: "/root/subdir/file.txt"
//
// # I/O Interface Conversions
//
// The package provides generic type conversion functions for common I/O interfaces.
// These are useful for type erasure when you need to work with interface types
// rather than concrete implementations:
//
// import (
// "bytes"
// "io"
// "github.com/IBM/fp-go/v2/file"
// )
//
// // Convert concrete types to interfaces
// buf := bytes.NewBuffer([]byte("hello"))
// var reader io.Reader = file.ToReader(buf)
//
// writer := &bytes.Buffer{}
// var w io.Writer = file.ToWriter(writer)
//
// f, _ := os.Open("file.txt")
// var closer io.Closer = file.ToCloser(f)
// defer closer.Close()
//
// # Design Philosophy
//
// The functions in this package follow functional programming principles:
//
// - Currying: Functions like Join return functions, enabling partial application
// - Type Safety: Generic functions maintain type safety while providing flexibility
// - Composability: All functions work well with fp-go's pipe and composition utilities
// - Immutability: Functions don't modify their inputs
//
// # Performance
//
// The type conversion functions (ToReader, ToWriter, ToCloser) have zero overhead
// as they simply return their input cast to the interface type. The Join function
// uses Go's standard filepath.Join internally, ensuring cross-platform compatibility.
package file

View File

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

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

@@ -0,0 +1,367 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package file
import (
"bytes"
"io"
"os"
"path/filepath"
"strings"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
func TestJoin(t *testing.T) {
t.Run("joins simple paths", func(t *testing.T) {
result := Join("config.json")("/etc/myapp")
expected := filepath.Join("/etc/myapp", "config.json")
assert.Equal(t, expected, result)
})
t.Run("joins with subdirectories", func(t *testing.T) {
result := Join("logs/app.log")("/var")
expected := filepath.Join("/var", "logs/app.log")
assert.Equal(t, expected, result)
})
t.Run("handles empty root", func(t *testing.T) {
result := Join("file.txt")("")
assert.Equal(t, "file.txt", result)
})
t.Run("handles empty name", func(t *testing.T) {
result := Join("")("/root")
expected := filepath.Join("/root", "")
assert.Equal(t, expected, result)
})
t.Run("handles relative paths", func(t *testing.T) {
result := Join("config.json")("./app")
expected := filepath.Join("./app", "config.json")
assert.Equal(t, expected, result)
})
t.Run("normalizes path separators", func(t *testing.T) {
result := Join("file.txt")("/root/path")
// Should use OS-specific separator
assert.Contains(t, result, "file.txt")
assert.Contains(t, result, "root")
assert.Contains(t, result, "path")
})
t.Run("works with Pipe", func(t *testing.T) {
result := F.Pipe1("/var/log", Join("app.log"))
expected := filepath.Join("/var/log", "app.log")
assert.Equal(t, expected, result)
})
t.Run("chains multiple joins", func(t *testing.T) {
result := F.Pipe2(
"/root",
Join("subdir"),
Join("file.txt"),
)
expected := filepath.Join("/root", "subdir", "file.txt")
assert.Equal(t, expected, result)
})
t.Run("handles special characters", func(t *testing.T) {
result := Join("my file.txt")("/path with spaces")
expected := filepath.Join("/path with spaces", "my file.txt")
assert.Equal(t, expected, result)
})
t.Run("handles dots in path", func(t *testing.T) {
result := Join("../config.json")("/app/current")
expected := filepath.Join("/app/current", "../config.json")
assert.Equal(t, expected, result)
})
}
func TestToReader(t *testing.T) {
t.Run("converts bytes.Buffer to io.Reader", func(t *testing.T) {
buf := bytes.NewBuffer([]byte("hello world"))
reader := ToReader(buf)
// Verify it's an io.Reader
var _ io.Reader = reader
// Verify it works
data, err := io.ReadAll(reader)
assert.NoError(t, err)
assert.Equal(t, "hello world", string(data))
})
t.Run("converts bytes.Reader to io.Reader", func(t *testing.T) {
bytesReader := bytes.NewReader([]byte("test data"))
reader := ToReader(bytesReader)
var _ io.Reader = reader
data, err := io.ReadAll(reader)
assert.NoError(t, err)
assert.Equal(t, "test data", string(data))
})
t.Run("converts strings.Reader to io.Reader", func(t *testing.T) {
strReader := strings.NewReader("string content")
reader := ToReader(strReader)
var _ io.Reader = reader
data, err := io.ReadAll(reader)
assert.NoError(t, err)
assert.Equal(t, "string content", string(data))
})
t.Run("preserves reader functionality", func(t *testing.T) {
original := bytes.NewBuffer([]byte("test"))
reader := ToReader(original)
// Read once
buf1 := make([]byte, 2)
n, err := reader.Read(buf1)
assert.NoError(t, err)
assert.Equal(t, 2, n)
assert.Equal(t, "te", string(buf1))
// Read again
buf2 := make([]byte, 2)
n, err = reader.Read(buf2)
assert.NoError(t, err)
assert.Equal(t, 2, n)
assert.Equal(t, "st", string(buf2))
})
t.Run("handles empty reader", func(t *testing.T) {
buf := bytes.NewBuffer([]byte{})
reader := ToReader(buf)
data, err := io.ReadAll(reader)
assert.NoError(t, err)
assert.Equal(t, "", string(data))
})
}
func TestToWriter(t *testing.T) {
t.Run("converts bytes.Buffer to io.Writer", func(t *testing.T) {
buf := &bytes.Buffer{}
writer := ToWriter(buf)
// Verify it's an io.Writer
var _ io.Writer = writer
// Verify it works
n, err := writer.Write([]byte("hello"))
assert.NoError(t, err)
assert.Equal(t, 5, n)
assert.Equal(t, "hello", buf.String())
})
t.Run("preserves writer functionality", func(t *testing.T) {
buf := &bytes.Buffer{}
writer := ToWriter(buf)
// Write multiple times
writer.Write([]byte("hello "))
writer.Write([]byte("world"))
assert.Equal(t, "hello world", buf.String())
})
t.Run("handles empty writes", func(t *testing.T) {
buf := &bytes.Buffer{}
writer := ToWriter(buf)
n, err := writer.Write([]byte{})
assert.NoError(t, err)
assert.Equal(t, 0, n)
assert.Equal(t, "", buf.String())
})
t.Run("handles large writes", func(t *testing.T) {
buf := &bytes.Buffer{}
writer := ToWriter(buf)
data := make([]byte, 10000)
for i := range data {
data[i] = byte('A' + (i % 26))
}
n, err := writer.Write(data)
assert.NoError(t, err)
assert.Equal(t, 10000, n)
assert.Equal(t, 10000, buf.Len())
})
}
func TestToCloser(t *testing.T) {
t.Run("converts file to io.Closer", func(t *testing.T) {
// Create a temporary file
tmpfile, err := os.CreateTemp("", "test")
assert.NoError(t, err)
defer os.Remove(tmpfile.Name())
closer := ToCloser(tmpfile)
// Verify it's an io.Closer
var _ io.Closer = closer
// Verify it works
err = closer.Close()
assert.NoError(t, err)
})
t.Run("converts nopCloser to io.Closer", func(t *testing.T) {
// Use io.NopCloser which is a standard implementation
reader := strings.NewReader("test")
nopCloser := io.NopCloser(reader)
closer := ToCloser(nopCloser)
var _ io.Closer = closer
err := closer.Close()
assert.NoError(t, err)
})
t.Run("preserves close functionality", func(t *testing.T) {
tmpfile, err := os.CreateTemp("", "test")
assert.NoError(t, err)
defer os.Remove(tmpfile.Name())
closer := ToCloser(tmpfile)
// Close should work
err = closer.Close()
assert.NoError(t, err)
// Subsequent operations should fail
_, err = tmpfile.Write([]byte("test"))
assert.Error(t, err)
})
}
// Test type conversions work together
func TestIntegration(t *testing.T) {
t.Run("reader and closer together", func(t *testing.T) {
tmpfile, err := os.CreateTemp("", "test")
assert.NoError(t, err)
defer os.Remove(tmpfile.Name())
// Write some data
tmpfile.Write([]byte("test content"))
tmpfile.Seek(0, 0)
// Convert to interfaces
reader := ToReader(tmpfile)
closer := ToCloser(tmpfile)
// Use as reader
data, err := io.ReadAll(reader)
assert.NoError(t, err)
assert.Equal(t, "test content", string(data))
// Close
err = closer.Close()
assert.NoError(t, err)
})
t.Run("writer and closer together", func(t *testing.T) {
tmpfile, err := os.CreateTemp("", "test")
assert.NoError(t, err)
defer os.Remove(tmpfile.Name())
// Convert to interfaces
writer := ToWriter(tmpfile)
closer := ToCloser(tmpfile)
// Use as writer
n, err := writer.Write([]byte("test data"))
assert.NoError(t, err)
assert.Equal(t, 9, n)
// Close
err = closer.Close()
assert.NoError(t, err)
// Verify data was written
data, err := os.ReadFile(tmpfile.Name())
assert.NoError(t, err)
assert.Equal(t, "test data", string(data))
})
t.Run("all conversions with file", func(t *testing.T) {
tmpfile, err := os.CreateTemp("", "test")
assert.NoError(t, err)
defer os.Remove(tmpfile.Name())
// File implements Reader, Writer, and Closer
var reader io.Reader = ToReader(tmpfile)
var writer io.Writer = ToWriter(tmpfile)
var closer io.Closer = ToCloser(tmpfile)
// All should be non-nil
assert.NotNil(t, reader)
assert.NotNil(t, writer)
assert.NotNil(t, closer)
// Write, read, close
writer.Write([]byte("hello"))
tmpfile.Seek(0, 0)
data, _ := io.ReadAll(reader)
assert.Equal(t, "hello", string(data))
closer.Close()
})
}
// Benchmark tests
func BenchmarkJoin(b *testing.B) {
joiner := Join("config.json")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = joiner("/etc/myapp")
}
}
func BenchmarkToReader(b *testing.B) {
buf := bytes.NewBuffer([]byte("test data"))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ToReader(buf)
}
}
func BenchmarkToWriter(b *testing.B) {
buf := &bytes.Buffer{}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ToWriter(buf)
}
}
func BenchmarkToCloser(b *testing.B) {
tmpfile, _ := os.CreateTemp("", "bench")
defer os.Remove(tmpfile.Name())
defer tmpfile.Close()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ToCloser(tmpfile)
}
}

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

@@ -0,0 +1,45 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package file
import "github.com/IBM/fp-go/v2/endomorphism"
type (
// Endomorphism represents a function from a type to itself: A -> A.
// This is a type alias for endomorphism.Endomorphism[A].
//
// In the context of the file package, this is used for functions that
// transform strings (paths) into strings (paths), such as the Join function.
//
// An endomorphism has useful algebraic properties:
// - Identity: There exists an identity endomorphism (the identity function)
// - Composition: Endomorphisms can be composed to form new endomorphisms
// - Associativity: Composition is associative
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// // Join returns an Endomorphism[string]
// addConfig := file.Join("config.json") // Endomorphism[string]
// addLogs := file.Join("logs") // Endomorphism[string]
//
// // Compose endomorphisms
// addConfigLogs := F.Flow2(addLogs, addConfig)
// result := addConfigLogs("/var")
// // result is "/var/logs/config.json"
Endomorphism[A any] = endomorphism.Endomorphism[A]
)

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,187 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerresult
import (
"context"
"fmt"
"strconv"
"testing"
N "github.com/IBM/fp-go/v2/number"
"github.com/stretchr/testify/assert"
)
// TestPromapBasic tests basic Promap functionality
func TestPromapBasic(t *testing.T) {
t.Run("transform both context and output", func(t *testing.T) {
// ReaderResult that reads a value from context
getValue := func(ctx context.Context) (int, error) {
if val := ctx.Value("port"); val != nil {
return val.(int), nil
}
return 0, fmt.Errorf("port not found")
}
// Transform context to add a value and int to string
addPort := func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithValue(ctx, "port", 8080), func() {}
}
toString := strconv.Itoa
adapted := Promap(addPort, toString)(getValue)
result, err := adapted(context.Background())
assert.NoError(t, err)
assert.Equal(t, "8080", result)
})
t.Run("handles error case", func(t *testing.T) {
// ReaderResult that returns an error
getError := func(ctx context.Context) (int, error) {
return 0, fmt.Errorf("error occurred")
}
addPort := func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithValue(ctx, "port", 8080), func() {}
}
toString := strconv.Itoa
adapted := Promap(addPort, toString)(getError)
_, err := adapted(context.Background())
assert.Error(t, err)
assert.Equal(t, "error occurred", err.Error())
})
t.Run("context transformation with cancellation", func(t *testing.T) {
getValue := func(ctx context.Context) (string, error) {
if val := ctx.Value("key"); val != nil {
return val.(string), nil
}
return "", fmt.Errorf("key not found")
}
addValue := func(ctx context.Context) (context.Context, context.CancelFunc) {
ctx, cancel := context.WithCancel(ctx)
return context.WithValue(ctx, "key", "value"), cancel
}
toUpper := func(s string) string {
return "UPPER_" + s
}
adapted := Promap(addValue, toUpper)(getValue)
result, err := adapted(context.Background())
assert.NoError(t, err)
assert.Equal(t, "UPPER_value", result)
})
}
// TestContramapBasic tests basic Contramap functionality
func TestContramapBasic(t *testing.T) {
t.Run("context adaptation", func(t *testing.T) {
// ReaderResult that reads from context
getPort := func(ctx context.Context) (int, error) {
if val := ctx.Value("port"); val != nil {
return val.(int), nil
}
return 0, fmt.Errorf("port not found")
}
// Adapt context to add port value
addPort := func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithValue(ctx, "port", 9000), func() {}
}
adapted := Contramap[int](addPort)(getPort)
result, err := adapted(context.Background())
assert.NoError(t, err)
assert.Equal(t, 9000, result)
})
t.Run("preserves error", func(t *testing.T) {
getError := func(ctx context.Context) (int, error) {
return 0, fmt.Errorf("config error")
}
addPort := func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithValue(ctx, "port", 9000), func() {}
}
adapted := Contramap[int](addPort)(getError)
_, err := adapted(context.Background())
assert.Error(t, err)
assert.Equal(t, "config error", err.Error())
})
t.Run("multiple context values", func(t *testing.T) {
getValues := func(ctx context.Context) (string, error) {
host := ctx.Value("host")
port := ctx.Value("port")
if host != nil && port != nil {
return fmt.Sprintf("%s:%d", host, port), nil
}
return "", fmt.Errorf("missing values")
}
addValues := func(ctx context.Context) (context.Context, context.CancelFunc) {
ctx = context.WithValue(ctx, "host", "localhost")
ctx = context.WithValue(ctx, "port", 8080)
return ctx, func() {}
}
adapted := Contramap[string](addValues)(getValues)
result, err := adapted(context.Background())
assert.NoError(t, err)
assert.Equal(t, "localhost:8080", result)
})
}
// TestPromapComposition tests that Promap can be composed
func TestPromapComposition(t *testing.T) {
t.Run("compose two Promap transformations", func(t *testing.T) {
reader := func(ctx context.Context) (int, error) {
if val := ctx.Value("value"); val != nil {
return val.(int), nil
}
return 0, fmt.Errorf("value not found")
}
f1 := func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithValue(ctx, "value", 5), func() {}
}
g1 := N.Mul(2)
f2 := func(ctx context.Context) (context.Context, context.CancelFunc) {
return ctx, func() {}
}
g2 := N.Add(10)
// Apply two Promap transformations
step1 := Promap(f1, g1)(reader)
step2 := Promap(f2, g2)(step1)
result, err := step2(context.Background())
// (5 * 2) + 10 = 20
assert.NoError(t, err)
assert.Equal(t, 20, result)
})
}

View File

@@ -0,0 +1,74 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerioresult
import (
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
"github.com/IBM/fp-go/v2/reader"
)
// Promap is the profunctor map operation that transforms both the input and output of a ReaderIOResult.
// It applies f to the input environment (contravariantly) and g to the output value (covariantly).
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// This operation allows you to:
// - Adapt the environment type before passing it to the ReaderIOResult (via f)
// - Transform the success value after the IO effect completes (via g)
//
// The error type is fixed as error and remains unchanged through the transformation.
//
// Type Parameters:
// - E: The original environment type expected by the ReaderIOResult
// - A: The original success type produced by the ReaderIOResult
// - D: The new input environment type
// - B: The new output success type
//
// Parameters:
// - f: Function to transform the input environment from D to E (contravariant)
// - g: Function to transform the output success value from A to B (covariant)
//
// Returns:
// - A Kleisli arrow that takes a ReaderIOResult[E, A] and returns a ReaderIOResult[D, B]
//
//go:inline
func Promap[E, A, D, B any](f func(D) E, g func(A) B) Kleisli[D, ReaderIOResult[E, A], B] {
return reader.Promap(f, ioresult.Map(g))
}
// Contramap changes the value of the local environment during the execution of a ReaderIOResult.
// This is the contravariant functor operation that transforms the input environment.
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// Contramap is useful for adapting a ReaderIOResult to work with a different environment type
// by providing a function that converts the new environment to the expected one.
//
// Type Parameters:
// - A: The success type (unchanged)
// - R2: The new input environment type
// - R1: The original environment type expected by the ReaderIOResult
//
// Parameters:
// - f: Function to transform the environment from R2 to R1
//
// Returns:
// - A Kleisli arrow that takes a ReaderIOResult[R1, A] and returns a ReaderIOResult[R2, A]
//
//go:inline
func Contramap[A, R1, R2 any](f func(R2) R1) Kleisli[R2, ReaderIOResult[R1, A], A] {
return Local[A](f)
}

View File

@@ -0,0 +1,199 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerioresult
import (
"fmt"
"strconv"
"testing"
N "github.com/IBM/fp-go/v2/number"
"github.com/stretchr/testify/assert"
)
type SimpleConfig struct {
Port int
}
type DetailedConfig struct {
Host string
Port int
}
// TestPromapBasic tests basic Promap functionality
func TestPromapBasic(t *testing.T) {
t.Run("transform both input and output", func(t *testing.T) {
// ReaderIOResult that reads port from SimpleConfig
getPort := func(c SimpleConfig) func() (int, error) {
return func() (int, error) {
return c.Port, nil
}
}
// Transform DetailedConfig to SimpleConfig and int to string
simplify := func(d DetailedConfig) SimpleConfig {
return SimpleConfig{Port: d.Port}
}
toString := strconv.Itoa
adapted := Promap(simplify, toString)(getPort)
result, err := adapted(DetailedConfig{Host: "localhost", Port: 8080})()
assert.NoError(t, err)
assert.Equal(t, "8080", result)
})
t.Run("handles error case", func(t *testing.T) {
// ReaderIOResult that returns an error
getError := func(c SimpleConfig) func() (int, error) {
return func() (int, error) {
return 0, fmt.Errorf("error occurred")
}
}
simplify := func(d DetailedConfig) SimpleConfig {
return SimpleConfig{Port: d.Port}
}
toString := strconv.Itoa
adapted := Promap(simplify, toString)(getError)
_, err := adapted(DetailedConfig{Host: "localhost", Port: 8080})()
assert.Error(t, err)
assert.Equal(t, "error occurred", err.Error())
})
}
// TestContramapBasic tests basic Contramap functionality
func TestContramapBasic(t *testing.T) {
t.Run("environment adaptation", func(t *testing.T) {
// ReaderIOResult that reads from SimpleConfig
getPort := func(c SimpleConfig) func() (int, error) {
return func() (int, error) {
return c.Port, nil
}
}
// Adapt to work with DetailedConfig
simplify := func(d DetailedConfig) SimpleConfig {
return SimpleConfig{Port: d.Port}
}
adapted := Contramap[int](simplify)(getPort)
result, err := adapted(DetailedConfig{Host: "localhost", Port: 9000})()
assert.NoError(t, err)
assert.Equal(t, 9000, result)
})
t.Run("preserves error", func(t *testing.T) {
getError := func(c SimpleConfig) func() (int, error) {
return func() (int, error) {
return 0, fmt.Errorf("config error")
}
}
simplify := func(d DetailedConfig) SimpleConfig {
return SimpleConfig{Port: d.Port}
}
adapted := Contramap[int](simplify)(getError)
_, err := adapted(DetailedConfig{Host: "localhost", Port: 9000})()
assert.Error(t, err)
assert.Equal(t, "config error", err.Error())
})
}
// TestPromapWithIO tests Promap with actual IO effects
func TestPromapWithIO(t *testing.T) {
t.Run("transform IO result", func(t *testing.T) {
counter := 0
// ReaderIOResult with side effect
getPortWithEffect := func(c SimpleConfig) func() (int, error) {
return func() (int, error) {
counter++
return c.Port, nil
}
}
simplify := func(d DetailedConfig) SimpleConfig {
return SimpleConfig{Port: d.Port}
}
toString := strconv.Itoa
adapted := Promap(simplify, toString)(getPortWithEffect)
result, err := adapted(DetailedConfig{Host: "localhost", Port: 8080})()
assert.NoError(t, err)
assert.Equal(t, "8080", result)
assert.Equal(t, 1, counter) // Side effect occurred
})
t.Run("side effect occurs even on error", func(t *testing.T) {
counter := 0
getErrorWithEffect := func(c SimpleConfig) func() (int, error) {
return func() (int, error) {
counter++
return 0, fmt.Errorf("io error")
}
}
simplify := func(d DetailedConfig) SimpleConfig {
return SimpleConfig{Port: d.Port}
}
toString := strconv.Itoa
adapted := Promap(simplify, toString)(getErrorWithEffect)
_, err := adapted(DetailedConfig{Host: "localhost", Port: 8080})()
assert.Error(t, err)
assert.Equal(t, 1, counter) // Side effect occurred before error
})
}
// TestPromapComposition tests that Promap can be composed
func TestPromapComposition(t *testing.T) {
t.Run("compose two Promap transformations", func(t *testing.T) {
type Config1 struct{ Value int }
type Config2 struct{ Value int }
type Config3 struct{ Value int }
reader := func(c Config1) func() (int, error) {
return func() (int, error) {
return c.Value, nil
}
}
f1 := func(c2 Config2) Config1 { return Config1{Value: c2.Value} }
g1 := N.Mul(2)
f2 := func(c3 Config3) Config2 { return Config2{Value: c3.Value} }
g2 := N.Add(10)
// Apply two Promap transformations
step1 := Promap(f1, g1)(reader)
step2 := Promap(f2, g2)(step1)
result, err := step2(Config3{Value: 5})()
// (5 * 2) + 10 = 20
assert.NoError(t, err)
assert.Equal(t, 20, result)
})
}

View File

@@ -0,0 +1,76 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerresult
import "github.com/IBM/fp-go/v2/idiomatic/result"
// Promap is the profunctor map operation that transforms both the input and output of a ReaderResult.
// It applies f to the input environment (contravariantly) and g to the output value (covariantly).
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// This operation allows you to:
// - Adapt the environment type before passing it to the ReaderResult (via f)
// - Transform the success value after the computation completes (via g)
//
// The error type is fixed as error and remains unchanged through the transformation.
//
// Type Parameters:
// - E: The original environment type expected by the ReaderResult
// - A: The original success type produced by the ReaderResult
// - D: The new input environment type
// - B: The new output success type
//
// Parameters:
// - f: Function to transform the input environment from D to E (contravariant)
// - g: Function to transform the output success value from A to B (covariant)
//
// Returns:
// - A Kleisli arrow that takes a ReaderResult[E, A] and returns a ReaderResult[D, B]
//
//go:inline
func Promap[E, A, D, B any](f func(D) E, g func(A) B) Kleisli[D, ReaderResult[E, A], B] {
mp := result.Map(g)
return func(rr ReaderResult[E, A]) ReaderResult[D, B] {
return func(d D) (B, error) {
return mp(rr(f(d)))
}
}
}
// Contramap changes the value of the local environment during the execution of a ReaderResult.
// This is the contravariant functor operation that transforms the input environment.
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// Contramap is useful for adapting a ReaderResult to work with a different environment type
// by providing a function that converts the new environment to the expected one.
//
// Type Parameters:
// - A: The success type (unchanged)
// - R2: The new input environment type
// - R1: The original environment type expected by the ReaderResult
//
// Parameters:
// - f: Function to transform the environment from R2 to R1
//
// Returns:
// - A Kleisli arrow that takes a ReaderResult[R1, A] and returns a ReaderResult[R2, A]
//
//go:inline
func Contramap[A, R1, R2 any](f func(R2) R1) Kleisli[R2, ReaderResult[R1, A], A] {
return Local[A](f)
}

View File

@@ -0,0 +1,238 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerresult
import (
"fmt"
"strconv"
"testing"
N "github.com/IBM/fp-go/v2/number"
R "github.com/IBM/fp-go/v2/reader"
"github.com/stretchr/testify/assert"
)
type SimpleConfig struct {
Port int
}
type DetailedConfig struct {
Host string
Port int
}
// TestPromapBasic tests basic Promap functionality
func TestPromapBasic(t *testing.T) {
t.Run("transform both input and output", func(t *testing.T) {
// ReaderResult that reads port from SimpleConfig
getPort := func(c SimpleConfig) (int, error) {
return c.Port, nil
}
// Transform DetailedConfig to SimpleConfig and int to string
simplify := func(d DetailedConfig) SimpleConfig {
return SimpleConfig{Port: d.Port}
}
toString := strconv.Itoa
adapted := Promap(simplify, toString)(getPort)
result, err := adapted(DetailedConfig{Host: "localhost", Port: 8080})
assert.NoError(t, err)
assert.Equal(t, "8080", result)
})
t.Run("handles error case", func(t *testing.T) {
// ReaderResult that returns an error
getError := func(c SimpleConfig) (int, error) {
return 0, fmt.Errorf("error occurred")
}
simplify := func(d DetailedConfig) SimpleConfig {
return SimpleConfig{Port: d.Port}
}
toString := strconv.Itoa
adapted := Promap(simplify, toString)(getError)
_, err := adapted(DetailedConfig{Host: "localhost", Port: 8080})
assert.Error(t, err)
assert.Equal(t, "error occurred", err.Error())
})
t.Run("environment transformation with complex types", func(t *testing.T) {
type Database struct {
ConnectionString string
}
type AppConfig struct {
DB Database
}
getConnection := func(db Database) (string, error) {
if db.ConnectionString == "" {
return "", fmt.Errorf("empty connection string")
}
return db.ConnectionString, nil
}
extractDB := func(cfg AppConfig) Database {
return cfg.DB
}
addPrefix := func(s string) string {
return "postgres://" + s
}
adapted := Promap(extractDB, addPrefix)(getConnection)
result, err := adapted(AppConfig{DB: Database{ConnectionString: "localhost:5432"}})
assert.NoError(t, err)
assert.Equal(t, "postgres://localhost:5432", result)
})
}
// TestContramapBasic tests basic Contramap functionality
func TestContramapBasic(t *testing.T) {
t.Run("environment adaptation", func(t *testing.T) {
// ReaderResult that reads from SimpleConfig
getPort := func(c SimpleConfig) (int, error) {
return c.Port, nil
}
// Adapt to work with DetailedConfig
simplify := func(d DetailedConfig) SimpleConfig {
return SimpleConfig{Port: d.Port}
}
adapted := Contramap[int](simplify)(getPort)
result, err := adapted(DetailedConfig{Host: "localhost", Port: 9000})
assert.NoError(t, err)
assert.Equal(t, 9000, result)
})
t.Run("preserves error", func(t *testing.T) {
getError := func(c SimpleConfig) (int, error) {
return 0, fmt.Errorf("config error")
}
simplify := func(d DetailedConfig) SimpleConfig {
return SimpleConfig{Port: d.Port}
}
adapted := Contramap[int](simplify)(getError)
_, err := adapted(DetailedConfig{Host: "localhost", Port: 9000})
assert.Error(t, err)
assert.Equal(t, "config error", err.Error())
})
t.Run("multiple field extraction", func(t *testing.T) {
type FullConfig struct {
Host string
Port int
Protocol string
}
getURL := func(c DetailedConfig) (string, error) {
return fmt.Sprintf("%s:%d", c.Host, c.Port), nil
}
extractHostPort := func(fc FullConfig) DetailedConfig {
return DetailedConfig{Host: fc.Host, Port: fc.Port}
}
adapted := Contramap[string](extractHostPort)(getURL)
result, err := adapted(FullConfig{Host: "example.com", Port: 443, Protocol: "https"})
assert.NoError(t, err)
assert.Equal(t, "example.com:443", result)
})
}
// TestPromapComposition tests that Promap can be composed
func TestPromapComposition(t *testing.T) {
t.Run("compose two Promap transformations", func(t *testing.T) {
type Config1 struct{ Value int }
type Config2 struct{ Value int }
type Config3 struct{ Value int }
reader := func(c Config1) (int, error) {
return c.Value, nil
}
f1 := func(c2 Config2) Config1 { return Config1{Value: c2.Value} }
g1 := N.Mul(2)
f2 := func(c3 Config3) Config2 { return Config2{Value: c3.Value} }
g2 := N.Add(10)
// Apply two Promap transformations
step1 := Promap(f1, g1)(reader)
step2 := Promap(f2, g2)(step1)
result, err := step2(Config3{Value: 5})
// (5 * 2) + 10 = 20
assert.NoError(t, err)
assert.Equal(t, 20, result)
})
t.Run("compose Promap and Contramap", func(t *testing.T) {
type Config1 struct{ Value int }
type Config2 struct{ Value int }
reader := func(c Config1) (int, error) {
return c.Value * 3, nil
}
// First apply Contramap
f1 := func(c2 Config2) Config1 { return Config1{Value: c2.Value} }
step1 := Contramap[int](f1)(reader)
// Then apply Promap
f2 := func(c2 Config2) Config2 { return c2 }
g2 := func(n int) string { return fmt.Sprintf("result: %d", n) }
step2 := Promap(f2, g2)(step1)
result, err := step2(Config2{Value: 7})
// 7 * 3 = 21
assert.NoError(t, err)
assert.Equal(t, "result: 21", result)
})
}
// TestPromapIdentityLaws tests profunctor identity laws
func TestPromapIdentityLaws(t *testing.T) {
t.Run("identity law", func(t *testing.T) {
// Promap with identity functions should be identity
reader := func(c SimpleConfig) (int, error) {
return c.Port, nil
}
identity := R.Ask[SimpleConfig]()
identityInt := R.Ask[int]()
adapted := Promap(identity, identityInt)(reader)
config := SimpleConfig{Port: 8080}
result1, err1 := reader(config)
result2, err2 := adapted(config)
assert.Equal(t, err1, err2)
assert.Equal(t, result1, result2)
})
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@@ -0,0 +1,322 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ord
import (
"testing"
"github.com/stretchr/testify/assert"
)
// Test Semigroup laws
func TestSemigroup_Associativity(t *testing.T) {
type Person struct {
LastName string
FirstName string
MiddleName string
}
stringOrd := FromStrictCompare[string]()
byLastName := Contramap(func(p Person) string { return p.LastName })(stringOrd)
byFirstName := Contramap(func(p Person) string { return p.FirstName })(stringOrd)
byMiddleName := Contramap(func(p Person) string { return p.MiddleName })(stringOrd)
sg := Semigroup[Person]()
// Test associativity: (a <> b) <> c == a <> (b <> c)
left := sg.Concat(sg.Concat(byLastName, byFirstName), byMiddleName)
right := sg.Concat(byLastName, sg.Concat(byFirstName, byMiddleName))
p1 := Person{LastName: "Smith", FirstName: "John", MiddleName: "A"}
p2 := Person{LastName: "Smith", FirstName: "John", MiddleName: "B"}
assert.Equal(t, left.Compare(p1, p2), right.Compare(p1, p2), "Associativity should hold")
}
// Test Semigroup with three levels
func TestSemigroup_ThreeLevels(t *testing.T) {
type Employee struct {
Department string
Level int
Name string
}
stringOrd := FromStrictCompare[string]()
intOrd := FromStrictCompare[int]()
byDept := Contramap(func(e Employee) string { return e.Department })(stringOrd)
byLevel := Contramap(func(e Employee) int { return e.Level })(intOrd)
byName := Contramap(func(e Employee) string { return e.Name })(stringOrd)
sg := Semigroup[Employee]()
employeeOrd := sg.Concat(sg.Concat(byDept, byLevel), byName)
e1 := Employee{Department: "IT", Level: 3, Name: "Alice"}
e2 := Employee{Department: "IT", Level: 3, Name: "Bob"}
e3 := Employee{Department: "IT", Level: 2, Name: "Charlie"}
e4 := Employee{Department: "HR", Level: 3, Name: "David"}
// Same dept, same level, different name
assert.Equal(t, -1, employeeOrd.Compare(e1, e2), "Alice < Bob")
// Same dept, different level
assert.Equal(t, 1, employeeOrd.Compare(e1, e3), "Level 3 > Level 2")
// Different dept
assert.Equal(t, -1, employeeOrd.Compare(e4, e1), "HR < IT")
}
// Test Monoid identity laws
func TestMonoid_IdentityLaws(t *testing.T) {
m := Monoid[int]()
intOrd := FromStrictCompare[int]()
emptyOrd := m.Empty()
// Left identity: empty <> x == x
leftIdentity := m.Concat(emptyOrd, intOrd)
assert.Equal(t, -1, leftIdentity.Compare(3, 5), "Left identity: 3 < 5")
assert.Equal(t, 1, leftIdentity.Compare(5, 3), "Left identity: 5 > 3")
// Right identity: x <> empty == x
rightIdentity := m.Concat(intOrd, emptyOrd)
assert.Equal(t, -1, rightIdentity.Compare(3, 5), "Right identity: 3 < 5")
assert.Equal(t, 1, rightIdentity.Compare(5, 3), "Right identity: 5 > 3")
}
// Test Monoid with multiple empty concatenations
func TestMonoid_MultipleEmpty(t *testing.T) {
m := Monoid[int]()
emptyOrd := m.Empty()
// Concatenating multiple empty orderings should still be empty
combined := m.Concat(m.Concat(emptyOrd, emptyOrd), emptyOrd)
assert.Equal(t, 0, combined.Compare(5, 3), "Multiple empties: always equal")
assert.Equal(t, 0, combined.Compare(3, 5), "Multiple empties: always equal")
assert.True(t, combined.Equals(5, 3), "Multiple empties: always equal")
}
// Test MaxSemigroup with edge cases
func TestMaxSemigroup_EdgeCases(t *testing.T) {
intOrd := FromStrictCompare[int]()
maxSg := MaxSemigroup(intOrd)
tests := []struct {
name string
a int
b int
expected int
}{
{"both positive", 5, 3, 5},
{"both negative", -5, -3, -3},
{"mixed signs", -5, 3, 3},
{"zero and positive", 0, 5, 5},
{"zero and negative", 0, -5, 0},
{"both zero", 0, 0, 0},
{"equal positive", 5, 5, 5},
{"equal negative", -5, -5, -5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := maxSg.Concat(tt.a, tt.b)
assert.Equal(t, tt.expected, result)
})
}
}
// Test MinSemigroup with edge cases
func TestMinSemigroup_EdgeCases(t *testing.T) {
intOrd := FromStrictCompare[int]()
minSg := MinSemigroup(intOrd)
tests := []struct {
name string
a int
b int
expected int
}{
{"both positive", 5, 3, 3},
{"both negative", -5, -3, -5},
{"mixed signs", -5, 3, -5},
{"zero and positive", 0, 5, 0},
{"zero and negative", 0, -5, -5},
{"both zero", 0, 0, 0},
{"equal positive", 5, 5, 5},
{"equal negative", -5, -5, -5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := minSg.Concat(tt.a, tt.b)
assert.Equal(t, tt.expected, result)
})
}
}
// Test MaxSemigroup with strings
func TestMaxSemigroup_Strings(t *testing.T) {
stringOrd := FromStrictCompare[string]()
maxSg := MaxSemigroup(stringOrd)
tests := []struct {
name string
a string
b string
expected string
}{
{"alphabetical", "apple", "banana", "banana"},
{"same string", "apple", "apple", "apple"},
{"empty and non-empty", "", "apple", "apple"},
{"both empty", "", "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := maxSg.Concat(tt.a, tt.b)
assert.Equal(t, tt.expected, result)
})
}
}
// Test MinSemigroup with strings
func TestMinSemigroup_Strings(t *testing.T) {
stringOrd := FromStrictCompare[string]()
minSg := MinSemigroup(stringOrd)
tests := []struct {
name string
a string
b string
expected string
}{
{"alphabetical", "apple", "banana", "apple"},
{"same string", "apple", "apple", "apple"},
{"empty and non-empty", "", "apple", ""},
{"both empty", "", "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := minSg.Concat(tt.a, tt.b)
assert.Equal(t, tt.expected, result)
})
}
}
// Test MaxSemigroup associativity
func TestMaxSemigroup_Associativity(t *testing.T) {
intOrd := FromStrictCompare[int]()
maxSg := MaxSemigroup(intOrd)
// (a <> b) <> c == a <> (b <> c)
a, b, c := 5, 3, 7
left := maxSg.Concat(maxSg.Concat(a, b), c)
right := maxSg.Concat(a, maxSg.Concat(b, c))
assert.Equal(t, left, right, "MaxSemigroup should be associative")
assert.Equal(t, 7, left, "Should return maximum value")
}
// Test MinSemigroup associativity
func TestMinSemigroup_Associativity(t *testing.T) {
intOrd := FromStrictCompare[int]()
minSg := MinSemigroup(intOrd)
// (a <> b) <> c == a <> (b <> c)
a, b, c := 5, 3, 7
left := minSg.Concat(minSg.Concat(a, b), c)
right := minSg.Concat(a, minSg.Concat(b, c))
assert.Equal(t, left, right, "MinSemigroup should be associative")
assert.Equal(t, 3, left, "Should return minimum value")
}
// Test Semigroup with reversed ordering
func TestSemigroup_WithReverse(t *testing.T) {
type Person struct {
Age int
Name string
}
intOrd := FromStrictCompare[int]()
stringOrd := FromStrictCompare[string]()
// Order by age descending, then by name ascending
byAge := Contramap(func(p Person) int { return p.Age })(Reverse(intOrd))
byName := Contramap(func(p Person) string { return p.Name })(stringOrd)
sg := Semigroup[Person]()
personOrd := sg.Concat(byAge, byName)
p1 := Person{Age: 30, Name: "Alice"}
p2 := Person{Age: 30, Name: "Bob"}
p3 := Person{Age: 25, Name: "Charlie"}
// Same age, different name
assert.Equal(t, -1, personOrd.Compare(p1, p2), "Alice < Bob (same age)")
// Different age (descending)
assert.Equal(t, -1, personOrd.Compare(p1, p3), "30 > 25 (descending)")
}
// Benchmark MaxSemigroup
func BenchmarkMaxSemigroup(b *testing.B) {
intOrd := FromStrictCompare[int]()
maxSg := MaxSemigroup(intOrd)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = maxSg.Concat(i, i+1)
}
}
// Benchmark MinSemigroup
func BenchmarkMinSemigroup(b *testing.B) {
intOrd := FromStrictCompare[int]()
minSg := MinSemigroup(intOrd)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = minSg.Concat(i, i+1)
}
}
// Benchmark Semigroup concatenation
func BenchmarkSemigroup_Concat(b *testing.B) {
type Person struct {
LastName string
FirstName string
}
stringOrd := FromStrictCompare[string]()
byLastName := Contramap(func(p Person) string { return p.LastName })(stringOrd)
byFirstName := Contramap(func(p Person) string { return p.FirstName })(stringOrd)
sg := Semigroup[Person]()
personOrd := sg.Concat(byLastName, byFirstName)
p1 := Person{LastName: "Smith", FirstName: "Alice"}
p2 := Person{LastName: "Smith", FirstName: "Bob"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = personOrd.Compare(p1, p2)
}
}
// Made with Bob

View File

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

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

@@ -0,0 +1,61 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ord
type (
// Kleisli represents a function that takes a value of type A and returns an Ord[B].
// This is useful for creating orderings that depend on input values.
//
// Type Parameters:
// - A: The input type
// - B: The type for which ordering is produced
//
// Example:
//
// // Create a Kleisli that produces different orderings based on input
// var orderingFactory Kleisli[string, int] = func(mode string) Ord[int] {
// if mode == "ascending" {
// return ord.FromStrictCompare[int]()
// }
// return ord.Reverse(ord.FromStrictCompare[int]())
// }
// ascOrd := orderingFactory("ascending")
// descOrd := orderingFactory("descending")
Kleisli[A, B any] = func(A) Ord[B]
// Operator represents a function that transforms an Ord[A] into a value of type B.
// This is commonly used for operations that modify or combine orderings.
//
// Type Parameters:
// - A: The type for which ordering is defined
// - B: The result type of the operation
//
// This is equivalent to Kleisli[Ord[A], B] and is used for operations like
// Contramap, which takes an Ord[A] and produces an Ord[B].
//
// Example:
//
// // Contramap is an Operator that transforms Ord[A] to Ord[B]
// type Person struct { Age int }
// var ageOperator Operator[int, Person] = ord.Contramap(func(p Person) int {
// return p.Age
// })
// intOrd := ord.FromStrictCompare[int]()
// personOrd := ageOperator(intOrd)
Operator[A, B any] = Kleisli[Ord[A], B]
)
// Made with Bob

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

@@ -0,0 +1,205 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ord
import (
"testing"
"github.com/stretchr/testify/assert"
)
// Test Kleisli type
func TestKleisli(t *testing.T) {
// Create a Kleisli that produces different orderings based on input
var orderingFactory Kleisli[string, int] = func(mode string) Ord[int] {
if mode == "ascending" {
return FromStrictCompare[int]()
}
return Reverse(FromStrictCompare[int]())
}
// Test ascending order
ascOrd := orderingFactory("ascending")
assert.Equal(t, -1, ascOrd.Compare(3, 5), "ascending: 3 < 5")
assert.Equal(t, 1, ascOrd.Compare(5, 3), "ascending: 5 > 3")
assert.Equal(t, 0, ascOrd.Compare(5, 5), "ascending: 5 == 5")
// Test descending order
descOrd := orderingFactory("descending")
assert.Equal(t, 1, descOrd.Compare(3, 5), "descending: 3 > 5")
assert.Equal(t, -1, descOrd.Compare(5, 3), "descending: 5 < 3")
assert.Equal(t, 0, descOrd.Compare(5, 5), "descending: 5 == 5")
}
// Test Kleisli with complex types
func TestKleisli_ComplexType(t *testing.T) {
type Person struct {
Name string
Age int
}
// Kleisli that creates orderings based on a field selector
var personOrderingFactory Kleisli[string, Person] = func(field string) Ord[Person] {
stringOrd := FromStrictCompare[string]()
intOrd := FromStrictCompare[int]()
switch field {
case "name":
return Contramap(func(p Person) string { return p.Name })(stringOrd)
case "age":
return Contramap(func(p Person) int { return p.Age })(intOrd)
default:
// Default to name ordering
return Contramap(func(p Person) string { return p.Name })(stringOrd)
}
}
p1 := Person{Name: "Alice", Age: 30}
p2 := Person{Name: "Bob", Age: 25}
// Order by name
nameOrd := personOrderingFactory("name")
assert.Equal(t, -1, nameOrd.Compare(p1, p2), "Alice < Bob by name")
// Order by age
ageOrd := personOrderingFactory("age")
assert.Equal(t, 1, ageOrd.Compare(p1, p2), "30 > 25 by age")
}
// Test Operator type
func TestOperator(t *testing.T) {
type Person struct {
Name string
Age int
}
// Operator that transforms Ord[int] to Ord[Person] by age
var ageOperator Operator[int, Person] = Contramap(func(p Person) int {
return p.Age
})
intOrd := FromStrictCompare[int]()
personOrd := ageOperator(intOrd)
p1 := Person{Name: "Alice", Age: 30}
p2 := Person{Name: "Bob", Age: 25}
p3 := Person{Name: "Charlie", Age: 30}
assert.Equal(t, 1, personOrd.Compare(p1, p2), "30 > 25")
assert.Equal(t, -1, personOrd.Compare(p2, p1), "25 < 30")
assert.Equal(t, 0, personOrd.Compare(p1, p3), "30 == 30")
assert.True(t, personOrd.Equals(p1, p3), "same age")
assert.False(t, personOrd.Equals(p1, p2), "different age")
}
// Test Operator composition
func TestOperator_Composition(t *testing.T) {
type Address struct {
Street string
City string
}
type Person struct {
Name string
Address Address
}
// Create operators for different transformations
stringOrd := FromStrictCompare[string]()
// Operator to order Person by city
var cityOperator Operator[string, Person] = Contramap(func(p Person) string {
return p.Address.City
})
personOrd := cityOperator(stringOrd)
p1 := Person{Name: "Alice", Address: Address{Street: "Main St", City: "Boston"}}
p2 := Person{Name: "Bob", Address: Address{Street: "Oak Ave", City: "Austin"}}
assert.Equal(t, 1, personOrd.Compare(p1, p2), "Boston > Austin")
assert.Equal(t, -1, personOrd.Compare(p2, p1), "Austin < Boston")
}
// Test Operator with multiple transformations
func TestOperator_MultipleTransformations(t *testing.T) {
type Product struct {
Name string
Price float64
}
floatOrd := FromStrictCompare[float64]()
// Operator to order by price
var priceOperator Operator[float64, Product] = Contramap(func(p Product) float64 {
return p.Price
})
// Operator to reverse the ordering
var reverseOperator Operator[float64, Product] = func(o Ord[float64]) Ord[Product] {
return priceOperator(Reverse(o))
}
// Order by price descending
productOrd := reverseOperator(floatOrd)
prod1 := Product{Name: "Widget", Price: 19.99}
prod2 := Product{Name: "Gadget", Price: 29.99}
assert.Equal(t, 1, productOrd.Compare(prod1, prod2), "19.99 > 29.99 (reversed)")
assert.Equal(t, -1, productOrd.Compare(prod2, prod1), "29.99 < 19.99 (reversed)")
}
// Example test for Kleisli
func ExampleKleisli() {
// Create a Kleisli that produces different orderings based on input
var orderingFactory Kleisli[string, int] = func(mode string) Ord[int] {
if mode == "ascending" {
return FromStrictCompare[int]()
}
return Reverse(FromStrictCompare[int]())
}
ascOrd := orderingFactory("ascending")
descOrd := orderingFactory("descending")
println(ascOrd.Compare(5, 3)) // 1
println(descOrd.Compare(5, 3)) // -1
}
// Example test for Operator
func ExampleOperator() {
type Person struct {
Name string
Age int
}
// Operator that transforms Ord[int] to Ord[Person] by age
var ageOperator Operator[int, Person] = Contramap(func(p Person) int {
return p.Age
})
intOrd := FromStrictCompare[int]()
personOrd := ageOperator(intOrd)
p1 := Person{Name: "Alice", Age: 30}
p2 := Person{Name: "Bob", Age: 25}
result := personOrd.Compare(p1, p2)
println(result) // 1 (30 > 25)
}
// Made with Bob

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,76 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package 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)
}

View 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)
})
}

View File

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

View 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)
})
}

View File

@@ -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()

View File

@@ -0,0 +1,76 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package 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)
}

View 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
})
}

View File

@@ -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()

View 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)
}

View File

@@ -0,0 +1,98 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerioresult
import (
"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)
})
}

View File

@@ -0,0 +1,74 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package 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)
}

View File

@@ -0,0 +1,106 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package 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)
})
}

View File

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

View File

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

View 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)
}

View 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))
})
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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'")
}

View File

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

View File

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

View 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"))
})
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) },
// )
//

View File

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

View File

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

View File

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