1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-02-24 12:57:26 +02:00

Compare commits

..

7 Commits

Author SHA1 Message Date
Dr. Carsten Leue
47727fd514 fix: add support for go 1.26
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-12 10:51:34 +01:00
Dr. Carsten Leue
ece7d088ea fix: add support for go 1.26
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-12 10:50:30 +01:00
Dr. Carsten Leue
13d25eca32 fix: add composition logic to Iso
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-12 10:46:41 +01:00
Dr. Carsten Leue
a68e32308d fix: add filterable to Either and Result
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-12 09:28:42 +01:00
Dr. Carsten Leue
61b948425b fix: cleaner use of Kleisli
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-11 16:24:11 +01:00
Dr. Carsten Leue
a276f3acff fix: add llms.txt
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-10 09:48:19 +01:00
Dr. Carsten Leue
8c656a4297 fix: more Alt tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-10 08:52:39 +01:00
36 changed files with 6651 additions and 74 deletions

View File

@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.20.x', '1.21.x', '1.22.x', '1.23.x', '1.24.x', '1.25.x']
go-version: ['1.20.x', '1.21.x', '1.22.x', '1.23.x', '1.24.x', '1.25.x', '1.26.x']
fail-fast: false # Continue with other versions if one fails
steps:
# full checkout for semantic-release
@@ -64,7 +64,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.24.x', '1.25.x']
go-version: ['1.24.x', '1.25.x', '1.26.x']
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:

View File

@@ -21,7 +21,7 @@ import (
"github.com/IBM/fp-go/v2/internal/array"
M "github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/tuple"
"github.com/IBM/fp-go/v2/pair"
)
// From constructs an array from a set of variadic arguments
@@ -163,11 +163,11 @@ func FilterMapWithIndex[A, B any](f func(int, A) Option[B]) Operator[A, B] {
return G.FilterMapWithIndex[[]A, []B](f)
}
// FilterChain maps an array with an iterating function that returns an [Option] of an array. It keeps only the Some values discarding the Nones and then flattens the result.
// ChainOptionK maps an array with an iterating function that returns an [Option] of an array. It keeps only the Some values discarding the Nones and then flattens the result.
//
//go:inline
func FilterChain[A, B any](f option.Kleisli[A, []B]) Operator[A, B] {
return G.FilterChain[[]A](f)
func ChainOptionK[A, B any](f option.Kleisli[A, []B]) Operator[A, B] {
return G.ChainOptionK[[]A](f)
}
// FilterMapRef filters an array using a predicate on pointers and maps the matching elements using a function on pointers.
@@ -453,7 +453,7 @@ func Size[A any](as []A) int {
// the second contains elements for which it returns true.
//
//go:inline
func MonadPartition[A any](as []A, pred func(A) bool) tuple.Tuple2[[]A, []A] {
func MonadPartition[A any](as []A, pred func(A) bool) pair.Pair[[]A, []A] {
return G.MonadPartition(as, pred)
}
@@ -461,7 +461,7 @@ func MonadPartition[A any](as []A, pred func(A) bool) tuple.Tuple2[[]A, []A] {
// for which the predicate returns false, the right one those for which the predicate returns true
//
//go:inline
func Partition[A any](pred func(A) bool) func([]A) tuple.Tuple2[[]A, []A] {
func Partition[A any](pred func(A) bool) func([]A) pair.Pair[[]A, []A] {
return G.Partition[[]A](pred)
}

View File

@@ -24,8 +24,8 @@ import (
"github.com/IBM/fp-go/v2/internal/utils"
N "github.com/IBM/fp-go/v2/number"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
S "github.com/IBM/fp-go/v2/string"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/stretchr/testify/assert"
)
@@ -163,11 +163,11 @@ func TestPartition(t *testing.T) {
return n > 2
}
assert.Equal(t, T.MakeTuple2(Empty[int](), Empty[int]()), Partition(pred)(Empty[int]()))
assert.Equal(t, T.MakeTuple2(From(1), From(3)), Partition(pred)(From(1, 3)))
assert.Equal(t, pair.MakePair(Empty[int](), Empty[int]()), Partition(pred)(Empty[int]()))
assert.Equal(t, pair.MakePair(From(1), From(3)), Partition(pred)(From(1, 3)))
}
func TestFilterChain(t *testing.T) {
func TestChainOptionK(t *testing.T) {
src := From(1, 2, 3)
f := func(i int) O.Option[[]string] {
@@ -177,7 +177,7 @@ func TestFilterChain(t *testing.T) {
return O.None[[]string]()
}
res := FilterChain(f)(src)
res := ChainOptionK(f)(src)
assert.Equal(t, From("a1", "b1", "a3", "b3"), res)
}

View File

@@ -21,7 +21,7 @@ import (
FC "github.com/IBM/fp-go/v2/internal/functor"
M "github.com/IBM/fp-go/v2/monoid"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/tuple"
"github.com/IBM/fp-go/v2/pair"
)
// Of constructs a single element array
@@ -215,7 +215,7 @@ func Filter[AS ~[]A, PRED ~func(A) bool, A any](pred PRED) func(AS) AS {
return FilterWithIndex[AS](F.Ignore1of2[int](pred))
}
func FilterChain[GA ~[]A, GB ~[]B, A, B any](f func(a A) O.Option[GB]) func(GA) GB {
func ChainOptionK[GA ~[]A, GB ~[]B, A, B any](f func(a A) O.Option[GB]) func(GA) GB {
return F.Flow2(
FilterMap[GA, []GB](f),
Flatten[[]GB],
@@ -234,7 +234,7 @@ func FilterMapWithIndex[GA ~[]A, GB ~[]B, A, B any](f func(int, A) O.Option[B])
return F.Bind2nd(MonadFilterMapWithIndex[GA, GB, A, B], f)
}
func MonadPartition[GA ~[]A, A any](as GA, pred func(A) bool) tuple.Tuple2[GA, GA] {
func MonadPartition[GA ~[]A, A any](as GA, pred func(A) bool) pair.Pair[GA, GA] {
left := Empty[GA]()
right := Empty[GA]()
array.Reduce(as, func(c bool, a A) bool {
@@ -246,10 +246,10 @@ func MonadPartition[GA ~[]A, A any](as GA, pred func(A) bool) tuple.Tuple2[GA, G
return c
}, true)
// returns the partition
return tuple.MakeTuple2(left, right)
return pair.MakePair(left, right)
}
func Partition[GA ~[]A, A any](pred func(A) bool) func(GA) tuple.Tuple2[GA, GA] {
func Partition[GA ~[]A, A any](pred func(A) bool) func(GA) pair.Pair[GA, GA] {
return F.Bind2nd(MonadPartition[GA, A], pred)
}

View File

@@ -18,7 +18,7 @@ package generic
import (
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/IBM/fp-go/v2/pair"
)
// ZipWith applies a function to pairs of elements at the same index in two arrays, collecting the results in a new array. If one
@@ -34,19 +34,19 @@ func ZipWith[AS ~[]A, BS ~[]B, CS ~[]C, FCT ~func(A, B) C, A, B, C any](fa AS, f
// Zip takes two arrays and returns an array of corresponding pairs. If one input array is short, excess elements of the
// longer array are discarded
func Zip[AS ~[]A, BS ~[]B, CS ~[]T.Tuple2[A, B], A, B any](fb BS) func(AS) CS {
return F.Bind23of3(ZipWith[AS, BS, CS, func(A, B) T.Tuple2[A, B]])(fb, T.MakeTuple2[A, B])
func Zip[AS ~[]A, BS ~[]B, CS ~[]pair.Pair[A, B], A, B any](fb BS) func(AS) CS {
return F.Bind23of3(ZipWith[AS, BS, CS, func(A, B) pair.Pair[A, B]])(fb, pair.MakePair[A, B])
}
// Unzip is the function is reverse of [Zip]. Takes an array of pairs and return two corresponding arrays
func Unzip[AS ~[]A, BS ~[]B, CS ~[]T.Tuple2[A, B], A, B any](cs CS) T.Tuple2[AS, BS] {
func Unzip[AS ~[]A, BS ~[]B, CS ~[]pair.Pair[A, B], A, B any](cs CS) pair.Pair[AS, BS] {
l := len(cs)
as := make(AS, l)
bs := make(BS, l)
for i := range l {
t := cs[i]
as[i] = t.F1
bs[i] = t.F2
as[i] = pair.Head(t)
bs[i] = pair.Tail(t)
}
return T.MakeTuple2(as, bs)
return pair.MakePair(as, bs)
}

View File

@@ -17,7 +17,7 @@ package array
import (
G "github.com/IBM/fp-go/v2/array/generic"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/IBM/fp-go/v2/pair"
)
// ZipWith applies a function to pairs of elements at the same index in two arrays,
@@ -55,8 +55,8 @@ func ZipWith[FCT ~func(A, B) C, A, B, C any](fa []A, fb []B, f FCT) []C {
// // Result: [(a, 1), (b, 2)]
//
//go:inline
func Zip[A, B any](fb []B) func([]A) []T.Tuple2[A, B] {
return G.Zip[[]A, []B, []T.Tuple2[A, B]](fb)
func Zip[A, B any](fb []B) func([]A) []pair.Pair[A, B] {
return G.Zip[[]A, []B, []pair.Pair[A, B]](fb)
}
// Unzip is the reverse of Zip. It takes an array of pairs (tuples) and returns
@@ -78,6 +78,6 @@ func Zip[A, B any](fb []B) func([]A) []T.Tuple2[A, B] {
// ages := result.Tail // [30, 25, 35]
//
//go:inline
func Unzip[A, B any](cs []T.Tuple2[A, B]) T.Tuple2[[]A, []B] {
func Unzip[A, B any](cs []pair.Pair[A, B]) pair.Pair[[]A, []B] {
return G.Unzip[[]A, []B](cs)
}

View File

@@ -19,7 +19,7 @@ import (
"fmt"
"testing"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/IBM/fp-go/v2/pair"
"github.com/stretchr/testify/assert"
)
@@ -40,7 +40,7 @@ func TestZip(t *testing.T) {
res := Zip[string](left)(right)
assert.Equal(t, From(T.MakeTuple2("a", 1), T.MakeTuple2("b", 2), T.MakeTuple2("c", 3)), res)
assert.Equal(t, From(pair.MakePair("a", 1), pair.MakePair("b", 2), pair.MakePair("c", 3)), res)
}
func TestUnzip(t *testing.T) {
@@ -51,6 +51,6 @@ func TestUnzip(t *testing.T) {
unzipped := Unzip(zipped)
assert.Equal(t, right, unzipped.F1)
assert.Equal(t, left, unzipped.F2)
assert.Equal(t, right, pair.Head(unzipped))
assert.Equal(t, left, pair.Tail(unzipped))
}

View File

@@ -87,8 +87,8 @@ var (
// assembleProviders constructs the provider map for item and non-item providers
assembleProviders = F.Flow3(
A.Partition(isItemProvider),
T.Map2(collectProviders, collectItemProviders),
T.Tupled2(mergeProviders.Concat),
pair.BiMap(collectProviders, collectItemProviders),
pair.Paired(mergeProviders.Concat),
)
)

351
v2/either/filterable.go Normal file
View File

@@ -0,0 +1,351 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package either provides implementations of the Either type and related operations.
//
// This package implements several Fantasy Land algebraic structures:
// - Filterable: https://github.com/fantasyland/fantasy-land#filterable
//
// The Filterable specification defines operations for filtering and partitioning
// data structures based on predicates and mapping functions.
package either
import (
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
)
// Partition separates an [Either] value into a [Pair] based on a predicate function.
// It returns a function that takes an Either and produces a Pair of Either values,
// where the first element contains values that fail the predicate and the second
// contains values that pass the predicate.
//
// This function implements the Filterable specification's partition operation:
// https://github.com/fantasyland/fantasy-land#filterable
//
// The behavior is as follows:
// - If the input is Left, both elements of the resulting Pair will be the same Left value
// - If the input is Right and the predicate returns true, the result is (Left(empty), Right(value))
// - If the input is Right and the predicate returns false, the result is (Right(value), Left(empty))
//
// This function is useful for separating Either values into two categories based on
// a condition, commonly used in filtering operations where you want to keep track of
// both the values that pass and fail a test.
//
// Parameters:
// - p: A predicate function that tests values of type A
// - empty: The default Left value to use when creating Left instances for partitioning
//
// Returns:
//
// A function that takes an Either[E, A] and returns a Pair where:
// - First element: Either values that fail the predicate (or original Left)
// - Second element: Either values that pass the predicate (or original Left)
//
// Example:
//
// import (
// E "github.com/IBM/fp-go/v2/either"
// N "github.com/IBM/fp-go/v2/number"
// P "github.com/IBM/fp-go/v2/pair"
// )
//
// // Partition positive and non-positive numbers
// isPositive := N.MoreThan(0)
// partition := E.Partition(isPositive, "not positive")
//
// // Right value that passes predicate
// result1 := partition(E.Right[string](5))
// // result1 = Pair(Left("not positive"), Right(5))
// left1, right1 := P.Unpack(result1)
// // left1 = Left("not positive"), right1 = Right(5)
//
// // Right value that fails predicate
// result2 := partition(E.Right[string](-3))
// // result2 = Pair(Right(-3), Left("not positive"))
// left2, right2 := P.Unpack(result2)
// // left2 = Right(-3), right2 = Left("not positive")
//
// // Left value passes through unchanged in both positions
// result3 := partition(E.Left[int]("error"))
// // result3 = Pair(Left("error"), Left("error"))
// left3, right3 := P.Unpack(result3)
// // left3 = Left("error"), right3 = Left("error")
func Partition[E, A any](p Predicate[A], empty E) func(Either[E, A]) Pair[Either[E, A], Either[E, A]] {
l := Left[A](empty)
return func(e Either[E, A]) Pair[Either[E, A], Either[E, A]] {
if e.isLeft {
return pair.Of(e)
}
if p(e.r) {
return pair.MakePair(l, e)
}
return pair.MakePair(e, l)
}
}
// Filter creates a filtering operation for [Either] values based on a predicate function.
// It returns a function that takes an Either and produces an Either, where Right values
// that fail the predicate are converted to Left values with the provided empty value.
//
// This function implements the Filterable specification's filter operation:
// https://github.com/fantasyland/fantasy-land#filterable
//
// The behavior is as follows:
// - If the input is Left, it passes through unchanged
// - If the input is Right and the predicate returns true, the Right value passes through unchanged
// - If the input is Right and the predicate returns false, it's converted to Left(empty)
//
// This function is useful for conditional validation or filtering of Either values,
// where you want to reject Right values that don't meet certain criteria by converting
// them to Left values with a default error.
//
// Parameters:
// - p: A predicate function that tests values of type A
// - empty: The default Left value to use when filtering out Right values that fail the predicate
//
// Returns:
//
// An Operator function that takes an Either[E, A] and returns an Either[E, A] where:
// - Left values pass through unchanged
// - Right values that pass the predicate remain as Right
// - Right values that fail the predicate become Left(empty)
//
// Example:
//
// import (
// E "github.com/IBM/fp-go/v2/either"
// N "github.com/IBM/fp-go/v2/number"
// )
//
// // Filter to keep only positive numbers
// isPositive := N.MoreThan(0)
// filterPositive := E.Filter(isPositive, "not positive")
//
// // Right value that passes predicate - remains Right
// result1 := filterPositive(E.Right[string](5))
// // result1 = Right(5)
//
// // Right value that fails predicate - becomes Left
// result2 := filterPositive(E.Right[string](-3))
// // result2 = Left("not positive")
//
// // Left value passes through unchanged
// result3 := filterPositive(E.Left[int]("original error"))
// // result3 = Left("original error")
//
// // Chaining filters
// isEven := func(n int) bool { return n%2 == 0 }
// filterEven := E.Filter(isEven, "not even")
//
// // Apply multiple filters in sequence
// result4 := filterEven(filterPositive(E.Right[string](4)))
// // result4 = Right(4) - passes both filters
//
// result5 := filterEven(filterPositive(E.Right[string](3)))
// // result5 = Left("not even") - passes first, fails second
func Filter[E, A any](p Predicate[A], empty E) Operator[E, A, A] {
l := Left[A](empty)
return func(e Either[E, A]) Either[E, A] {
if e.isLeft || p(e.r) {
return e
}
return l
}
}
// FilterMap combines filtering and mapping operations for [Either] values using an [Option]-returning function.
// It returns a function that takes an Either[E, A] and produces an Either[E, B], where Right values
// are transformed by applying the function f. If f returns Some(B), the result is Right(B). If f returns
// None, the result is Left(empty).
//
// This function implements the Filterable specification's filterMap operation:
// https://github.com/fantasyland/fantasy-land#filterable
//
// The behavior is as follows:
// - If the input is Left, it passes through with its error value preserved as Left[B]
// - If the input is Right and f returns Some(B), the result is Right(B)
// - If the input is Right and f returns None, the result is Left(empty)
//
// This function is useful for operations that combine validation/filtering with transformation,
// such as parsing strings to numbers (where invalid strings result in None), or extracting
// optional fields from structures.
//
// Parameters:
// - f: An Option Kleisli function that transforms values of type A to Option[B]
// - empty: The default Left value to use when f returns None
//
// Returns:
//
// An Operator function that takes an Either[E, A] and returns an Either[E, B] where:
// - Left values pass through with error preserved
// - Right values are transformed by f: Some(B) becomes Right(B), None becomes Left(empty)
//
// Example:
//
// import (
// E "github.com/IBM/fp-go/v2/either"
// O "github.com/IBM/fp-go/v2/option"
// "strconv"
// )
//
// // Parse string to int, filtering out invalid values
// parseInt := func(s string) O.Option[int] {
// if n, err := strconv.Atoi(s); err == nil {
// return O.Some(n)
// }
// return O.None[int]()
// }
// filterMapInt := E.FilterMap(parseInt, "invalid number")
//
// // Valid number string - transforms to Right(int)
// result1 := filterMapInt(E.Right[string]("42"))
// // result1 = Right(42)
//
// // Invalid number string - becomes Left
// result2 := filterMapInt(E.Right[string]("abc"))
// // result2 = Left("invalid number")
//
// // Left value passes through with error preserved
// result3 := filterMapInt(E.Left[string]("original error"))
// // result3 = Left("original error")
//
// // Extract optional field from struct
// type Person struct {
// Name string
// Email O.Option[string]
// }
// extractEmail := func(p Person) O.Option[string] { return p.Email }
// filterMapEmail := E.FilterMap(extractEmail, "no email")
//
// result4 := filterMapEmail(E.Right[string](Person{Name: "Alice", Email: O.Some("alice@example.com")}))
// // result4 = Right("alice@example.com")
//
// result5 := filterMapEmail(E.Right[string](Person{Name: "Bob", Email: O.None[string]()}))
// // result5 = Left("no email")
func FilterMap[E, A, B any](f option.Kleisli[A, B], empty E) Operator[E, A, B] {
l := Left[B](empty)
return func(e Either[E, A]) Either[E, B] {
if e.isLeft {
return Left[B](e.l)
}
if b, ok := option.Unwrap(f(e.r)); ok {
return Right[E](b)
}
return l
}
}
// PartitionMap separates and transforms an [Either] value into a [Pair] of Either values using a mapping function.
// It returns a function that takes an Either[E, A] and produces a Pair of Either values, where the mapping
// function f transforms the Right value into Either[B, C]. The result is partitioned based on whether f
// produces a Left or Right value.
//
// This function implements the Filterable specification's partitionMap operation:
// https://github.com/fantasyland/fantasy-land#filterable
//
// The behavior is as follows:
// - If the input is Left, both elements of the resulting Pair will be Left with the original error
// - If the input is Right and f returns Left(B), the result is (Right(B), Left(empty))
// - If the input is Right and f returns Right(C), the result is (Left(empty), Right(C))
//
// This function is useful for operations that need to categorize and transform values simultaneously,
// such as separating valid and invalid data while applying different transformations to each category.
//
// Parameters:
// - f: A Kleisli function that transforms values of type A to Either[B, C]
// - empty: The default error value to use when creating Left instances for partitioning
//
// Returns:
//
// A function that takes an Either[E, A] and returns a Pair[Either[E, B], Either[E, C]] where:
// - If input is Left: (Left(original_error), Left(original_error))
// - If f returns Left(B): (Right(B), Left(empty))
// - If f returns Right(C): (Left(empty), Right(C))
//
// Example:
//
// import (
// E "github.com/IBM/fp-go/v2/either"
// P "github.com/IBM/fp-go/v2/pair"
// )
//
// // Classify and transform numbers: negative -> error message, positive -> squared value
// classifyNumber := func(n int) E.Either[string, int] {
// if n < 0 {
// return E.Left[int]("negative: " + strconv.Itoa(n))
// }
// return E.Right[string](n * n)
// }
// partitionMap := E.PartitionMap(classifyNumber, "not classified")
//
// // Positive number - goes to right side as squared value
// result1 := partitionMap(E.Right[string](5))
// // result1 = Pair(Left("not classified"), Right(25))
// left1, right1 := P.Unpack(result1)
// // left1 = Left("not classified"), right1 = Right(25)
//
// // Negative number - goes to left side with error message
// result2 := partitionMap(E.Right[string](-3))
// // result2 = Pair(Right("negative: -3"), Left("not classified"))
// left2, right2 := P.Unpack(result2)
// // left2 = Right("negative: -3"), right2 = Left("not classified")
//
// // Original Left value - appears in both positions
// result3 := partitionMap(E.Left[int]("original error"))
// // result3 = Pair(Left("original error"), Left("original error"))
// left3, right3 := P.Unpack(result3)
// // left3 = Left("original error"), right3 = Left("original error")
//
// // Validate and transform user input
// type ValidationError struct{ Field, Message string }
// type User struct{ Name string; Age int }
//
// validateUser := func(input map[string]string) E.Either[ValidationError, User] {
// name, hasName := input["name"]
// ageStr, hasAge := input["age"]
// if !hasName {
// return E.Left[User](ValidationError{"name", "missing"})
// }
// if !hasAge {
// return E.Left[User](ValidationError{"age", "missing"})
// }
// age, err := strconv.Atoi(ageStr)
// if err != nil {
// return E.Left[User](ValidationError{"age", "invalid"})
// }
// return E.Right[ValidationError](User{name, age})
// }
// partitionUsers := E.PartitionMap(validateUser, ValidationError{"", "not processed"})
//
// validInput := map[string]string{"name": "Alice", "age": "30"}
// result4 := partitionUsers(E.Right[string](validInput))
// // result4 = Pair(Left(ValidationError{"", "not processed"}), Right(User{"Alice", 30}))
//
// invalidInput := map[string]string{"name": "Bob"}
// result5 := partitionUsers(E.Right[string](invalidInput))
// // result5 = Pair(Right(ValidationError{"age", "missing"}), Left(ValidationError{"", "not processed"}))
func PartitionMap[E, A, B, C any](f Kleisli[B, A, C], empty E) func(Either[E, A]) Pair[Either[E, B], Either[E, C]] {
return func(e Either[E, A]) Pair[Either[E, B], Either[E, C]] {
if e.isLeft {
return pair.MakePair(Left[B](e.l), Left[C](e.l))
}
res := f(e.r)
if res.isLeft {
return pair.MakePair(Right[E](res.l), Left[C](empty))
}
return pair.MakePair(Left[B](empty), Right[E](res.r))
}
}

1433
v2/either/filterable_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,7 @@ import (
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/reader"
)
@@ -53,4 +54,6 @@ type (
// Predicate represents a function that tests a value of type A and returns a boolean.
// It's commonly used for filtering and conditional operations.
Predicate[A any] = predicate.Predicate[A]
Pair[L, R any] = pair.Pair[L, R]
)

View File

@@ -466,6 +466,11 @@ func Chain[A, B any](f func(A) Seq[B]) Operator[A, B] {
return F.Bind2nd(MonadChain[A, B], f)
}
//go:inline
func FlatMap[A, B any](f func(A) Seq[B]) Operator[A, B] {
return Chain(f)
}
// Flatten flattens a sequence of sequences into a single sequence.
//
// RxJS Equivalent: [mergeAll] - https://rxjs.dev/api/operators/mergeAll

158
v2/iterator/iter/option.go Normal file
View File

@@ -0,0 +1,158 @@
// 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 iter
import (
"github.com/IBM/fp-go/v2/option"
)
// MonadChainOptionK chains a function that returns an Option into a sequence,
// filtering out None values and unwrapping Some values.
//
// This is useful for operations that may or may not produce a value for each element
// in the sequence. Only the successful (Some) results are included in the output sequence,
// while None values are filtered out.
//
// This is the monadic form that takes the sequence as the first parameter.
//
// RxJS Equivalent: [concatMap] combined with [filter] - https://rxjs.dev/api/operators/concatMap
//
// Type parameters:
// - A: The element type of the input sequence
// - B: The element type of the output sequence (wrapped in Option by the function)
//
// Parameters:
// - as: The input sequence to transform
// - f: A function that takes an element and returns an Option[B]
//
// Returns:
//
// A new sequence containing only the unwrapped Some values
//
// Example:
//
// import (
// "strconv"
// F "github.com/IBM/fp-go/v2/function"
// O "github.com/IBM/fp-go/v2/option"
// I "github.com/IBM/fp-go/v2/iterator/iter"
// )
//
// // Parse strings to integers, filtering out invalid ones
// parseNum := func(s string) O.Option[int] {
// if n, err := strconv.Atoi(s); err == nil {
// return O.Some(n)
// }
// return O.None[int]()
// }
//
// seq := I.From("1", "invalid", "2", "3", "bad")
// result := I.MonadChainOptionK(seq, parseNum)
// // yields: 1, 2, 3 (invalid strings are filtered out)
func MonadChainOptionK[A, B any](as Seq[A], f option.Kleisli[A, B]) Seq[B] {
return MonadFilterMap(as, f)
}
// ChainOptionK returns an operator that chains a function returning an Option into a sequence,
// filtering out None values and unwrapping Some values.
//
// This is the curried version of [MonadChainOptionK], useful for function composition
// and creating reusable transformations.
//
// RxJS Equivalent: [concatMap] combined with [filter] - https://rxjs.dev/api/operators/concatMap
//
// Type parameters:
// - A: The element type of the input sequence
// - B: The element type of the output sequence (wrapped in Option by the function)
//
// Parameters:
// - f: A function that takes an element and returns an Option[B]
//
// Returns:
//
// An Operator that transforms Seq[A] to Seq[B], filtering out None values
//
// Example:
//
// import (
// "strconv"
// F "github.com/IBM/fp-go/v2/function"
// O "github.com/IBM/fp-go/v2/option"
// I "github.com/IBM/fp-go/v2/iterator/iter"
// )
//
// // Create a reusable parser operator
// parsePositive := I.ChainOptionK(func(x int) O.Option[int] {
// if x > 0 {
// return O.Some(x)
// }
// return O.None[int]()
// })
//
// result := F.Pipe1(
// I.From(-1, 2, -3, 4, 5),
// parsePositive,
// )
// // yields: 2, 4, 5 (negative numbers are filtered out)
//
//go:inline
func ChainOptionK[A, B any](f option.Kleisli[A, B]) Operator[A, B] {
return FilterMap(f)
}
// FlatMapOptionK is an alias for [ChainOptionK].
//
// This provides a more familiar name for developers coming from other functional
// programming languages or libraries where "flatMap" is the standard terminology
// for the monadic bind operation.
//
// Type parameters:
// - A: The element type of the input sequence
// - B: The element type of the output sequence (wrapped in Option by the function)
//
// Parameters:
// - f: A function that takes an element and returns an Option[B]
//
// Returns:
//
// An Operator that transforms Seq[A] to Seq[B], filtering out None values
//
// Example:
//
// import (
// F "github.com/IBM/fp-go/v2/function"
// O "github.com/IBM/fp-go/v2/option"
// I "github.com/IBM/fp-go/v2/iterator/iter"
// )
//
// // Validate and transform data
// validateAge := I.FlatMapOptionK(func(age int) O.Option[string] {
// if age >= 18 && age <= 120 {
// return O.Some(fmt.Sprintf("Valid age: %d", age))
// }
// return O.None[string]()
// })
//
// result := F.Pipe1(
// I.From(15, 25, 150, 30),
// validateAge,
// )
// // yields: "Valid age: 25", "Valid age: 30"
//
//go:inline
func FlatMapOptionK[A, B any](f option.Kleisli[A, B]) Operator[A, B] {
return ChainOptionK(f)
}

View File

@@ -0,0 +1,387 @@
// 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 iter
import (
"fmt"
"slices"
"strconv"
"testing"
A "github.com/IBM/fp-go/v2/array"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestMonadChainOptionK_AllSome tests MonadChainOptionK when all values produce Some
func TestMonadChainOptionK_AllSome(t *testing.T) {
// Function that always returns Some
double := func(x int) O.Option[int] {
return O.Some(x * 2)
}
seq := From(1, 2, 3, 4, 5)
result := MonadChainOptionK(seq, double)
values := slices.Collect(result)
expected := A.From(2, 4, 6, 8, 10)
assert.Equal(t, expected, values)
}
// TestMonadChainOptionK_AllNone tests MonadChainOptionK when all values produce None
func TestMonadChainOptionK_AllNone(t *testing.T) {
// Function that always returns None
alwaysNone := func(x int) O.Option[int] {
return O.None[int]()
}
seq := From(1, 2, 3, 4, 5)
result := MonadChainOptionK(seq, alwaysNone)
values := slices.Collect(result)
assert.Empty(t, values)
}
// TestMonadChainOptionK_MixedSomeNone tests MonadChainOptionK with mixed Some and None
func TestMonadChainOptionK_MixedSomeNone(t *testing.T) {
// Function that returns Some for even numbers, None for odd
evenOnly := func(x int) O.Option[int] {
if x%2 == 0 {
return O.Some(x)
}
return O.None[int]()
}
seq := From(1, 2, 3, 4, 5, 6)
result := MonadChainOptionK(seq, evenOnly)
values := slices.Collect(result)
expected := A.From(2, 4, 6)
assert.Equal(t, expected, values)
}
// TestMonadChainOptionK_ParseStrings tests parsing strings to integers
func TestMonadChainOptionK_ParseStrings(t *testing.T) {
// Parse strings to integers, returning None for invalid strings
parseNum := func(s string) O.Option[int] {
if n, err := strconv.Atoi(s); err == nil {
return O.Some(n)
}
return O.None[int]()
}
seq := From("1", "invalid", "2", "3", "bad", "4")
result := MonadChainOptionK(seq, parseNum)
values := slices.Collect(result)
expected := A.From(1, 2, 3, 4)
assert.Equal(t, expected, values)
}
// TestMonadChainOptionK_EmptySequence tests MonadChainOptionK with empty sequence
func TestMonadChainOptionK_EmptySequence(t *testing.T) {
double := func(x int) O.Option[int] {
return O.Some(x * 2)
}
seq := From[int]()
result := MonadChainOptionK(seq, double)
values := slices.Collect(result)
assert.Empty(t, values)
}
// TestMonadChainOptionK_TypeTransformation tests transforming types
func TestMonadChainOptionK_TypeTransformation(t *testing.T) {
// Convert integers to strings, only for positive numbers
positiveToString := func(x int) O.Option[string] {
if x > 0 {
return O.Some(fmt.Sprintf("num_%d", x))
}
return O.None[string]()
}
seq := From(-2, -1, 0, 1, 2, 3)
result := MonadChainOptionK(seq, positiveToString)
values := slices.Collect(result)
expected := A.From("num_1", "num_2", "num_3")
assert.Equal(t, expected, values)
}
// TestMonadChainOptionK_ComplexType tests with complex types
func TestMonadChainOptionK_ComplexType(t *testing.T) {
type Person struct {
Name string
Age int
}
// Extract age only for adults
getAdultAge := func(p Person) O.Option[int] {
if p.Age >= 18 {
return O.Some(p.Age)
}
return O.None[int]()
}
seq := From(
Person{"Alice", 25},
Person{"Bob", 15},
Person{"Charlie", 30},
Person{"David", 12},
)
result := MonadChainOptionK(seq, getAdultAge)
values := slices.Collect(result)
expected := A.From(25, 30)
assert.Equal(t, expected, values)
}
// TestChainOptionK_BasicUsage tests ChainOptionK basic functionality
func TestChainOptionK_BasicUsage(t *testing.T) {
// Create a reusable operator
parsePositive := ChainOptionK(func(x int) O.Option[int] {
if x > 0 {
return O.Some(x)
}
return O.None[int]()
})
seq := From(-1, 2, -3, 4, 5, -6)
result := parsePositive(seq)
values := slices.Collect(result)
expected := A.From(2, 4, 5)
assert.Equal(t, expected, values)
}
// TestChainOptionK_WithPipe tests ChainOptionK in a pipeline
func TestChainOptionK_WithPipe(t *testing.T) {
// Validate and transform in a pipeline
validateRange := ChainOptionK(func(x int) O.Option[int] {
if x >= 0 && x <= 100 {
return O.Some(x)
}
return O.None[int]()
})
result := F.Pipe2(
From(-10, 20, 150, 50, 200, 75),
validateRange,
Map(func(x int) int { return x * 2 }),
)
values := slices.Collect(result)
expected := A.From(40, 100, 150)
assert.Equal(t, expected, values)
}
// TestChainOptionK_Composition tests composing multiple ChainOptionK operations
func TestChainOptionK_Composition(t *testing.T) {
// First filter: only positive
onlyPositive := ChainOptionK(func(x int) O.Option[int] {
if x > 0 {
return O.Some(x)
}
return O.None[int]()
})
// Second filter: only even
onlyEven := ChainOptionK(func(x int) O.Option[int] {
if x%2 == 0 {
return O.Some(x)
}
return O.None[int]()
})
result := F.Pipe2(
From(-2, -1, 0, 1, 2, 3, 4, 5, 6),
onlyPositive,
onlyEven,
)
values := slices.Collect(result)
expected := A.From(2, 4, 6)
assert.Equal(t, expected, values)
}
// TestChainOptionK_StringParsing tests parsing with ChainOptionK
func TestChainOptionK_StringParsing(t *testing.T) {
// Create a reusable string parser
parseInt := ChainOptionK(func(s string) O.Option[int] {
if n, err := strconv.Atoi(s); err == nil {
return O.Some(n)
}
return O.None[int]()
})
result := F.Pipe1(
From("10", "abc", "20", "xyz", "30"),
parseInt,
)
values := slices.Collect(result)
expected := A.From(10, 20, 30)
assert.Equal(t, expected, values)
}
// TestFlatMapOptionK_Equivalence tests that FlatMapOptionK is equivalent to ChainOptionK
func TestFlatMapOptionK_Equivalence(t *testing.T) {
validate := func(x int) O.Option[int] {
if x >= 0 && x <= 10 {
return O.Some(x)
}
return O.None[int]()
}
seq := From(-5, 0, 5, 10, 15)
// Using ChainOptionK
result1 := ChainOptionK(validate)(seq)
values1 := slices.Collect(result1)
// Using FlatMapOptionK
result2 := FlatMapOptionK(validate)(seq)
values2 := slices.Collect(result2)
// Both should produce the same result
assert.Equal(t, values1, values2)
assert.Equal(t, A.From(0, 5, 10), values1)
}
// TestFlatMapOptionK_WithMap tests FlatMapOptionK combined with Map
func TestFlatMapOptionK_WithMap(t *testing.T) {
// Validate age and convert to category
validateAge := FlatMapOptionK(func(age int) O.Option[string] {
if age >= 18 && age <= 120 {
return O.Some(fmt.Sprintf("Valid age: %d", age))
}
return O.None[string]()
})
result := F.Pipe1(
From(15, 25, 150, 30, 200),
validateAge,
)
values := slices.Collect(result)
expected := A.From("Valid age: 25", "Valid age: 30")
assert.Equal(t, expected, values)
}
// TestChainOptionK_LookupOperation tests using ChainOptionK for lookup operations
func TestChainOptionK_LookupOperation(t *testing.T) {
// Simulate a lookup table
lookup := map[string]int{
"one": 1,
"two": 2,
"three": 3,
}
lookupValue := ChainOptionK(func(key string) O.Option[int] {
if val, ok := lookup[key]; ok {
return O.Some(val)
}
return O.None[int]()
})
result := F.Pipe1(
From("one", "invalid", "two", "missing", "three"),
lookupValue,
)
values := slices.Collect(result)
expected := A.From(1, 2, 3)
assert.Equal(t, expected, values)
}
// TestMonadChainOptionK_EarlyTermination tests that iteration stops when yield returns false
func TestMonadChainOptionK_EarlyTermination(t *testing.T) {
callCount := 0
countCalls := func(x int) O.Option[int] {
callCount++
return O.Some(x)
}
seq := From(1, 2, 3, 4, 5)
result := MonadChainOptionK(seq, countCalls)
// Collect only first 3 elements
collected := make([]int, 0)
for v := range result {
collected = append(collected, v)
if len(collected) >= 3 {
break
}
}
// Should have called the function only 3 times due to early termination
assert.Equal(t, 3, callCount)
assert.Equal(t, A.From(1, 2, 3), collected)
}
// TestChainOptionK_WithReduce tests ChainOptionK with reduction
func TestChainOptionK_WithReduce(t *testing.T) {
// Parse and sum valid numbers
parseInt := ChainOptionK(func(s string) O.Option[int] {
if n, err := strconv.Atoi(s); err == nil {
return O.Some(n)
}
return O.None[int]()
})
result := F.Pipe1(
From("10", "invalid", "20", "bad", "30"),
parseInt,
)
sum := MonadReduce(result, func(acc, x int) int {
return acc + x
}, 0)
assert.Equal(t, 60, sum)
}
// TestFlatMapOptionK_NestedOptions tests FlatMapOptionK with nested option handling
func TestFlatMapOptionK_NestedOptions(t *testing.T) {
type Result struct {
Value int
Valid bool
}
// Extract value only if valid
extractValid := FlatMapOptionK(func(r Result) O.Option[int] {
if r.Valid {
return O.Some(r.Value)
}
return O.None[int]()
})
seq := From(
Result{10, true},
Result{20, false},
Result{30, true},
Result{40, false},
Result{50, true},
)
result := F.Pipe1(seq, extractValid)
values := slices.Collect(result)
expected := A.From(10, 30, 50)
assert.Equal(t, expected, values)
}

99
v2/llms.txt Normal file
View File

@@ -0,0 +1,99 @@
# fp-go
> A comprehensive functional programming library for Go, bringing type-safe monads, functors, applicatives, optics, and composable abstractions inspired by fp-ts and Haskell to the Go ecosystem. Created by IBM, licensed under Apache-2.0.
fp-go v2 requires Go 1.24+ and leverages generic type aliases for a cleaner API.
Key concepts: `Option` for nullable values, `Either`/`Result` for error handling, `IO` for lazy side effects, `Reader` for dependency injection, `IOResult` for effectful error handling, `ReaderIOResult` for the full monad stack, and `Optics` (lens, prism, traversal, iso) for immutable data manipulation.
## Core Documentation
- [API Reference (pkg.go.dev)](https://pkg.go.dev/github.com/IBM/fp-go/v2): Complete API documentation for all packages
- [README](https://github.com/IBM/fp-go/blob/main/v2/README.md): Overview, quick start, installation, and migration guide from v1 to v2
- [Design Decisions](https://github.com/IBM/fp-go/blob/main/v2/DESIGN.md): Key design principles and patterns
- [Functional I/O Guide](https://github.com/IBM/fp-go/blob/main/v2/FUNCTIONAL_IO.md): Understanding Context, errors, and the Reader pattern for I/O operations
- [Idiomatic vs Standard Comparison](https://github.com/IBM/fp-go/blob/main/v2/IDIOMATIC_COMPARISON.md): Performance comparison and when to use each approach
- [Optics README](https://github.com/IBM/fp-go/blob/main/v2/optics/README.md): Guide to lens, prism, optional, and traversal optics
## Standard Packages (struct-based)
- [option](https://pkg.go.dev/github.com/IBM/fp-go/v2/option): Option monad — represent optional values without nil
- [either](https://pkg.go.dev/github.com/IBM/fp-go/v2/either): Either monad — type-safe error handling with Left/Right values
- [result](https://pkg.go.dev/github.com/IBM/fp-go/v2/result): Result monad — simplified Either with `error` as Left type (recommended for error handling)
- [io](https://pkg.go.dev/github.com/IBM/fp-go/v2/io): IO monad — lazy evaluation and side effect management
- [iooption](https://pkg.go.dev/github.com/IBM/fp-go/v2/iooption): IOOption — IO combined with Option
- [ioeither](https://pkg.go.dev/github.com/IBM/fp-go/v2/ioeither): IOEither — IO combined with Either for effectful error handling
- [ioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/ioresult): IOResult — IO combined with Result (recommended over IOEither)
- [reader](https://pkg.go.dev/github.com/IBM/fp-go/v2/reader): Reader monad — dependency injection pattern
- [readeroption](https://pkg.go.dev/github.com/IBM/fp-go/v2/readeroption): ReaderOption — Reader combined with Option
- [readeriooption](https://pkg.go.dev/github.com/IBM/fp-go/v2/readeriooption): ReaderIOOption — Reader + IO + Option
- [readerioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/readerioresult): ReaderIOResult — Reader + IO + Result for complex workflows
- [readerioeither](https://pkg.go.dev/github.com/IBM/fp-go/v2/readerioeither): ReaderIOEither — Reader + IO + Either
- [statereaderioeither](https://pkg.go.dev/github.com/IBM/fp-go/v2/statereaderioeither): StateReaderIOEither — State + Reader + IO + Either
## Idiomatic Packages (tuple-based, high performance)
- [idiomatic/option](https://pkg.go.dev/github.com/IBM/fp-go/v2/idiomatic/option): Option using native Go `(value, bool)` tuples
- [idiomatic/result](https://pkg.go.dev/github.com/IBM/fp-go/v2/idiomatic/result): Result using native Go `(value, error)` tuples
- [idiomatic/ioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/idiomatic/ioresult): IOResult using `func() (value, error)`
- [idiomatic/readerresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/idiomatic/readerresult): ReaderResult with tuple-based results
- [idiomatic/readerioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/idiomatic/readerioresult): ReaderIOResult with tuple-based results
## Context Packages (context.Context specializations)
- [context/readerioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/context/readerioresult): ReaderIOResult specialized for context.Context
- [context/readerioresult/http](https://pkg.go.dev/github.com/IBM/fp-go/v2/context/readerioresult/http): Functional HTTP client utilities
- [context/readerioresult/http/builder](https://pkg.go.dev/github.com/IBM/fp-go/v2/context/readerioresult/http/builder): Functional HTTP request builder
- [context/statereaderioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/context/statereaderioresult): State + Reader + IO + Result for context.Context
## Optics
- [optics](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics): Core optics package
- [optics/lens](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/lens): Lenses for focusing on fields in product types
- [optics/prism](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/prism): Prisms for focusing on variants in sum types
- [optics/iso](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/iso): Isomorphisms for bidirectional transformations
- [optics/optional](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/optional): Optionals for values that may not exist
- [optics/traversal](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/traversal): Traversals for focusing on multiple values
- [optics/codec](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/codec): Codecs for encoding/decoding with validation
## Utility Packages
- [array](https://pkg.go.dev/github.com/IBM/fp-go/v2/array): Functional array/slice operations (map, filter, fold, etc.)
- [record](https://pkg.go.dev/github.com/IBM/fp-go/v2/record): Functional operations for maps
- [function](https://pkg.go.dev/github.com/IBM/fp-go/v2/function): Function composition, pipe, flow, curry, identity
- [pair](https://pkg.go.dev/github.com/IBM/fp-go/v2/pair): Strongly-typed pair/tuple data structure
- [tuple](https://pkg.go.dev/github.com/IBM/fp-go/v2/tuple): Type-safe heterogeneous tuples
- [predicate](https://pkg.go.dev/github.com/IBM/fp-go/v2/predicate): Predicate combinators (and, or, not, etc.)
- [endomorphism](https://pkg.go.dev/github.com/IBM/fp-go/v2/endomorphism): Endomorphism operations (compose, chain)
- [eq](https://pkg.go.dev/github.com/IBM/fp-go/v2/eq): Type-safe equality comparisons
- [ord](https://pkg.go.dev/github.com/IBM/fp-go/v2/ord): Total ordering type class
- [semigroup](https://pkg.go.dev/github.com/IBM/fp-go/v2/semigroup): Semigroup algebraic structure
- [monoid](https://pkg.go.dev/github.com/IBM/fp-go/v2/monoid): Monoid algebraic structure
- [number](https://pkg.go.dev/github.com/IBM/fp-go/v2/number): Algebraic structures for numeric types
- [string](https://pkg.go.dev/github.com/IBM/fp-go/v2/string): Functional string utilities
- [boolean](https://pkg.go.dev/github.com/IBM/fp-go/v2/boolean): Functional boolean utilities
- [bytes](https://pkg.go.dev/github.com/IBM/fp-go/v2/bytes): Functional byte slice utilities
- [json](https://pkg.go.dev/github.com/IBM/fp-go/v2/json): Functional JSON encoding/decoding
- [lazy](https://pkg.go.dev/github.com/IBM/fp-go/v2/lazy): Lazy evaluation without side effects
- [identity](https://pkg.go.dev/github.com/IBM/fp-go/v2/identity): Identity monad
- [retry](https://pkg.go.dev/github.com/IBM/fp-go/v2/retry): Retry policies with configurable backoff
- [tailrec](https://pkg.go.dev/github.com/IBM/fp-go/v2/tailrec): Trampoline for tail-call optimization
- [di](https://pkg.go.dev/github.com/IBM/fp-go/v2/di): Dependency injection utilities
- [effect](https://pkg.go.dev/github.com/IBM/fp-go/v2/effect): Functional effect system
- [circuitbreaker](https://pkg.go.dev/github.com/IBM/fp-go/v2/circuitbreaker): Circuit breaker error types
- [builder](https://pkg.go.dev/github.com/IBM/fp-go/v2/builder): Generic builder pattern with validation
## Code Samples
- [samples/builder](https://github.com/IBM/fp-go/tree/main/v2/samples/builder): Functional builder pattern example
- [samples/http](https://github.com/IBM/fp-go/tree/main/v2/samples/http): HTTP client examples
- [samples/lens](https://github.com/IBM/fp-go/tree/main/v2/samples/lens): Optics/lens examples
- [samples/mostly-adequate](https://github.com/IBM/fp-go/tree/main/v2/samples/mostly-adequate): Examples adapted from "Mostly Adequate Guide to Functional Programming"
- [samples/tuples](https://github.com/IBM/fp-go/tree/main/v2/samples/tuples): Tuple usage examples
## Optional
- [Source Code](https://github.com/IBM/fp-go): GitHub repository
- [Issues](https://github.com/IBM/fp-go/issues): Bug reports and feature requests
- [Go Report Card](https://goreportcard.com/report/github.com/IBM/fp-go/v2): Code quality report
- [Coverage](https://coveralls.io/github/IBM/fp-go?branch=main): Test coverage report

View File

@@ -13,6 +13,50 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package monoid provides an implementation of the Monoid algebraic structure.
//
// # Monoid
//
// A Monoid is an algebraic structure that extends [Semigroup] by adding an identity element.
// It consists of:
// - A type A
// - An associative binary operation Concat: (A, A) → A
// - An identity element Empty: () → A
//
// # Laws
//
// A Monoid must satisfy the following laws:
//
// 1. Associativity (from Semigroup):
// Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
//
// 2. Left Identity:
// Concat(Empty(), x) = x
//
// 3. Right Identity:
// Concat(x, Empty()) = x
//
// # Common Examples
//
// - Integer addition: Concat = (+), Empty = 0
// - Integer multiplication: Concat = (*), Empty = 1
// - String concatenation: Concat = (++), Empty = ""
// - List concatenation: Concat = (++), Empty = []
// - Boolean AND: Concat = (&&), Empty = true
// - Boolean OR: Concat = (||), Empty = false
// - Function composition: Concat = (∘), Empty = id
//
// # References
//
// - Haskell Data.Monoid: https://hackage.haskell.org/package/base/docs/Data-Monoid.html
// - Fantasy Land Monoid: https://github.com/fantasyland/fantasy-land#monoid
// - Semigroup: https://github.com/IBM/fp-go/v2/semigroup
//
// # Related Concepts
//
// - [Semigroup]: A Monoid without the identity element requirement
// - Magma: A set with a binary operation (no laws required)
// - Group: A Monoid where every element has an inverse
package monoid
import (
@@ -21,20 +65,31 @@ import (
// Monoid represents an algebraic structure with an associative binary operation and an identity element.
//
// A Monoid extends Semigroup by adding an identity element (Empty) that satisfies:
// A Monoid extends [Semigroup] by adding an identity element (Empty) that satisfies:
// - Left identity: Concat(Empty(), x) = x
// - Right identity: Concat(x, Empty()) = x
//
// The Monoid must also satisfy the associativity law from Semigroup:
// - Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
//
// Common examples:
// # Methods
//
// - Concat(x, y A) A: Inherited from Semigroup, combines two values associatively
// - Empty() A: Returns the identity element for the monoid
//
// # Common Examples
//
// - Integer addition with 0 as identity
// - Integer multiplication with 1 as identity
// - String concatenation with "" as identity
// - List concatenation with [] as identity
// - Boolean AND with true as identity
// - Boolean OR with false as identity
//
// # References
//
// - Haskell Monoid typeclass: https://hackage.haskell.org/package/base/docs/Data-Monoid.html#t:Monoid
// - Fantasy Land Monoid specification: https://github.com/fantasyland/fantasy-land#monoid
type Monoid[A any] interface {
S.Semigroup[A]
Empty() A
@@ -58,16 +113,22 @@ func (m monoid[A]) Empty() A {
// The provided concat function must be associative, and the empty element must
// satisfy the identity laws (left and right identity).
//
// Parameters:
// - c: An associative binary operation func(A, A) A
// - e: The identity element of type A
// This is the primary constructor for creating custom monoid instances. It's the
// equivalent of defining a Monoid instance in Haskell or implementing the Fantasy Land
// Monoid specification.
//
// Returns:
// - A Monoid[A] instance
// # Parameters
//
// Example:
// - c: An associative binary operation func(A, A) A (equivalent to Haskell's mappend or <>)
// - e: The identity element of type A (equivalent to Haskell's mempty)
//
// // Integer addition monoid
// # Returns
//
// - A [Monoid][A] instance
//
// # Example
//
// // Integer addition monoid (Sum in Haskell)
// addMonoid := MakeMonoid(
// func(a, b int) int { return a + b },
// 0, // identity element
@@ -81,6 +142,11 @@ func (m monoid[A]) Empty() A {
// "", // identity element
// )
// result := stringMonoid.Concat("Hello", " World") // "Hello World"
//
// # References
//
// - Haskell Monoid instance: https://hackage.haskell.org/package/base/docs/Data-Monoid.html#t:Monoid
// - Fantasy Land Monoid.empty: https://github.com/fantasyland/fantasy-land#monoid
func MakeMonoid[A any](c func(A, A) A, e A) Monoid[A] {
return monoid[A]{c: c, e: e}
}
@@ -91,13 +157,18 @@ func MakeMonoid[A any](c func(A, A) A, e A) Monoid[A] {
// operation in the opposite order. This is useful for operations that are
// not commutative.
//
// Parameters:
// This corresponds to the Dual newtype wrapper in Haskell's Data.Monoid, which
// provides a Monoid instance with reversed operation order.
//
// # Parameters
//
// - m: The monoid to reverse
//
// Returns:
// - A new Monoid[A] with reversed operation order
// # Returns
//
// Example:
// - A new [Monoid][A] with reversed operation order
//
// # Example
//
// // Subtraction monoid (not commutative)
// subMonoid := MakeMonoid(
@@ -116,6 +187,10 @@ func MakeMonoid[A any](c func(A, A) A, e A) Monoid[A] {
// )
// reversed := Reverse(stringMonoid)
// result := reversed.Concat("Hello", "World") // "WorldHello"
//
// # References
//
// - Haskell Data.Monoid.Dual: https://hackage.haskell.org/package/base/docs/Data-Monoid.html#t:Dual
func Reverse[A any](m Monoid[A]) Monoid[A] {
return MakeMonoid(S.Reverse(m).Concat, m.Empty())
}
@@ -125,13 +200,19 @@ func Reverse[A any](m Monoid[A]) Monoid[A] {
// This is useful when you need to use a monoid in a context that only requires
// a semigroup (associative binary operation without identity).
//
// Parameters:
// Since every Monoid is also a Semigroup (Monoid extends Semigroup), this conversion
// is always safe. This reflects the mathematical relationship where monoids form a
// subset of semigroups.
//
// # Parameters
//
// - m: The monoid to convert
//
// Returns:
// - A Semigroup[A] that uses the same Concat operation
// # Returns
//
// Example:
// - A [Semigroup][A] that uses the same Concat operation
//
// # Example
//
// addMonoid := MakeMonoid(
// func(a, b int) int { return a + b },
@@ -139,6 +220,11 @@ func Reverse[A any](m Monoid[A]) Monoid[A] {
// )
// sg := ToSemigroup(addMonoid)
// result := sg.Concat(5, 3) // 8 (identity not available)
//
// # References
//
// - Haskell Semigroup: https://hackage.haskell.org/package/base/docs/Data-Semigroup.html
// - Fantasy Land Semigroup: https://github.com/fantasyland/fantasy-land#semigroup
func ToSemigroup[A any](m Monoid[A]) S.Semigroup[A] {
return S.Semigroup[A](m)
}

View File

@@ -20,6 +20,7 @@ import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/optics/codec/validate"
"github.com/IBM/fp-go/v2/reader"
)
@@ -270,3 +271,210 @@ func MonadAlt[A, O, I any](first Type[A, O, I], second Lazy[Type[A, O, I]]) Type
func Alt[A, O, I any](second Lazy[Type[A, O, I]]) Operator[A, A, O, I] {
return F.Bind2nd(MonadAlt, second)
}
// AltMonoid creates a Monoid instance for Type[A, O, I] using alternative semantics
// with a provided zero/default codec.
//
// This function creates a monoid where:
// 1. The first successful codec wins (no result combination)
// 2. If the first fails during validation, the second is tried as a fallback
// 3. If both fail, errors are aggregated
// 4. The provided zero codec serves as the identity element
//
// Unlike other monoid patterns, AltMonoid does NOT combine successful results - it always
// returns the first success. This makes it ideal for building fallback chains with default
// codecs, configuration loading from multiple sources, and parser combinators with alternatives.
//
// # Type Parameters
//
// - A: The target type that all codecs decode to
// - O: The output type that all codecs encode to
// - I: The input type that all codecs decode from
//
// # Parameters
//
// - zero: A lazy Type[A, O, I] that serves as the identity element. This is typically
// a codec that always succeeds with a default value, but can also be a failing
// codec if no default is appropriate.
//
// # Returns
//
// A Monoid[Type[A, O, I]] that combines codecs using alternative semantics where
// the first success wins.
//
// # Behavior Details
//
// The AltMonoid implements a "first success wins" strategy:
//
// - **First succeeds**: Returns the first result, second is never evaluated
// - **First fails, second succeeds**: Returns the second result
// - **Both fail**: Aggregates errors from both validators
// - **Concat with Empty**: The zero codec is used as fallback
// - **Encoding**: Always uses the first codec's encoder
//
// # Example: Configuration Loading with Fallbacks
//
// import (
// "github.com/IBM/fp-go/v2/optics/codec"
// "github.com/IBM/fp-go/v2/array"
// )
//
// // Create a monoid with a default configuration
// m := codec.AltMonoid(func() codec.Type[Config, string, string] {
// return codec.MakeType(
// "DefaultConfig",
// codec.Is[Config](),
// func(s string) codec.Decode[codec.Context, Config] {
// return func(c codec.Context) codec.Validation[Config] {
// return validation.Success(defaultConfig)
// }
// },
// encodeConfig,
// )
// })
//
// // Define codecs for different sources
// fileCodec := loadFromFile("config.json")
// envCodec := loadFromEnv()
// defaultCodec := m.Empty()
//
// // Try file, then env, then default
// configCodec := array.MonadFold(
// []codec.Type[Config, string, string]{fileCodec, envCodec, defaultCodec},
// m.Empty(),
// m.Concat,
// )
//
// // Load configuration - tries each source in order
// result := configCodec.Decode(input)
//
// # Example: Parser with Multiple Formats
//
// // Create a monoid for parsing dates in multiple formats
// m := codec.AltMonoid(func() codec.Type[time.Time, string, string] {
// return codec.Date(time.RFC3339) // default format
// })
//
// // Define parsers for different date formats
// iso8601 := codec.Date("2006-01-02")
// usFormat := codec.Date("01/02/2006")
// euroFormat := codec.Date("02/01/2006")
//
// // Combine: try ISO 8601, then US, then European, then RFC3339
// flexibleDate := m.Concat(
// m.Concat(
// m.Concat(iso8601, usFormat),
// euroFormat,
// ),
// m.Empty(),
// )
//
// // Can parse any of these formats
// result1 := flexibleDate.Decode("2024-03-15") // ISO 8601
// result2 := flexibleDate.Decode("03/15/2024") // US format
// result3 := flexibleDate.Decode("15/03/2024") // European format
//
// # Example: Integer Parsing with Default
//
// // Create a monoid with default value of 0
// m := codec.AltMonoid(func() codec.Type[int, string, string] {
// return codec.MakeType(
// "DefaultZero",
// codec.Is[int](),
// func(s string) codec.Decode[codec.Context, int] {
// return func(c codec.Context) codec.Validation[int] {
// return validation.Success(0)
// }
// },
// strconv.Itoa,
// )
// })
//
// // Try parsing as int, fall back to 0
// intOrZero := m.Concat(codec.IntFromString(), m.Empty())
//
// result1 := intOrZero.Decode("42") // Success(42)
// result2 := intOrZero.Decode("invalid") // Success(0) - uses default
//
// # Example: Error Aggregation
//
// // Both codecs fail - errors are aggregated
// m := codec.AltMonoid(func() codec.Type[int, string, string] {
// return codec.MakeType(
// "NoDefault",
// codec.Is[int](),
// func(s string) codec.Decode[codec.Context, int] {
// return func(c codec.Context) codec.Validation[int] {
// return validation.FailureWithMessage[int](s, "no default available")(c)
// }
// },
// strconv.Itoa,
// )
// })
//
// failing1 := codec.MakeType(
// "Failing1",
// codec.Is[int](),
// func(s string) codec.Decode[codec.Context, int] {
// return func(c codec.Context) codec.Validation[int] {
// return validation.FailureWithMessage[int](s, "error 1")(c)
// }
// },
// strconv.Itoa,
// )
//
// failing2 := codec.MakeType(
// "Failing2",
// codec.Is[int](),
// func(s string) codec.Decode[codec.Context, int] {
// return func(c codec.Context) codec.Validation[int] {
// return validation.FailureWithMessage[int](s, "error 2")(c)
// }
// },
// strconv.Itoa,
// )
//
// combined := m.Concat(failing1, failing2)
// result := combined.Decode("input")
// // result contains errors: "error 1", "error 2", and "no default available"
//
// # Monoid Laws
//
// AltMonoid satisfies the monoid laws:
//
// 1. **Left Identity**: m.Concat(m.Empty(), codec) ≡ codec
// 2. **Right Identity**: m.Concat(codec, m.Empty()) ≡ codec (tries codec first, falls back to zero)
// 3. **Associativity**: m.Concat(m.Concat(a, b), c) ≡ m.Concat(a, m.Concat(b, c))
//
// Note: Due to the "first success wins" behavior, right identity means the zero is only
// used if the codec fails.
//
// # Use Cases
//
// - Configuration loading with multiple sources (file, env, default)
// - Parsing data in multiple formats with fallbacks
// - API versioning (try v2, fall back to v1, then default)
// - Content negotiation (try JSON, then XML, then plain text)
// - Validation with default values
// - Parser combinators with alternative branches
//
// # Notes
//
// - The zero codec is lazily evaluated, only when needed
// - First success short-circuits evaluation (subsequent codecs not tried)
// - Error aggregation ensures all validation failures are reported
// - Encoding always uses the first codec's encoder
// - This follows the alternative functor laws
//
// # See Also
//
// - MonadAlt: The underlying alternative operation for two codecs
// - Alt: The curried version for pipeline composition
// - validate.AltMonoid: The validation-level alternative monoid
// - decode.AltMonoid: The decode-level alternative monoid
func AltMonoid[A, O, I any](zero Lazy[Type[A, O, I]]) Monoid[Type[A, O, I]] {
return monoid.AltMonoid(
zero,
MonadAlt[A, O, I],
)
}

View File

@@ -561,3 +561,361 @@ func TestAltErrorMessages(t *testing.T) {
assert.True(t, hasCodec2Error, "should have error from second codec")
})
}
// TestAltMonoid tests the AltMonoid function
func TestAltMonoid(t *testing.T) {
t.Run("with default value as zero", func(t *testing.T) {
// Create a monoid with a default value of 0
m := AltMonoid(func() Type[int, string, string] {
return MakeType(
"DefaultZero",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(0)
}
},
strconv.Itoa,
)
})
// Create codecs
intFromString := IntFromString()
failing := MakeType(
"Failing",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.FailureWithMessage[int](s, "always fails")(c)
}
},
strconv.Itoa,
)
t.Run("first success wins", func(t *testing.T) {
// Combine two successful codecs - first should win
codec1 := MakeType(
"Returns10",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(10)
}
},
strconv.Itoa,
)
codec2 := MakeType(
"Returns20",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(20)
}
},
strconv.Itoa,
)
combined := m.Concat(codec1, codec2)
result := combined.Decode("input")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](0))(result)
assert.Equal(t, 10, value, "first success should win")
})
t.Run("falls back to second when first fails", func(t *testing.T) {
combined := m.Concat(failing, intFromString)
result := combined.Decode("42")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](0))(result)
assert.Equal(t, 42, value)
})
t.Run("uses zero when both fail", func(t *testing.T) {
combined := m.Concat(failing, m.Empty())
result := combined.Decode("invalid")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](-1))(result)
assert.Equal(t, 0, value, "should use default zero value")
})
})
t.Run("with failing zero", func(t *testing.T) {
// Create a monoid with a failing zero
m := AltMonoid(func() Type[int, string, string] {
return MakeType(
"NoDefault",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.FailureWithMessage[int](s, "no default available")(c)
}
},
strconv.Itoa,
)
})
failing1 := MakeType(
"Failing1",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.FailureWithMessage[int](s, "error 1")(c)
}
},
strconv.Itoa,
)
failing2 := MakeType(
"Failing2",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.FailureWithMessage[int](s, "error 2")(c)
}
},
strconv.Itoa,
)
t.Run("aggregates all errors when all fail", func(t *testing.T) {
combined := m.Concat(m.Concat(failing1, failing2), m.Empty())
result := combined.Decode("input")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(int) validation.Errors { return nil },
)
require.NotNil(t, errors)
// Should have errors from all three: failing1, failing2, and zero
assert.GreaterOrEqual(t, len(errors), 3)
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
hasError1 := false
hasError2 := false
hasNoDefault := false
for _, msg := range messages {
if msg == "error 1" {
hasError1 = true
}
if msg == "error 2" {
hasError2 = true
}
if msg == "no default available" {
hasNoDefault = true
}
}
assert.True(t, hasError1, "should have error from failing1")
assert.True(t, hasError2, "should have error from failing2")
assert.True(t, hasNoDefault, "should have error from zero")
})
})
t.Run("chaining multiple fallbacks", func(t *testing.T) {
m := AltMonoid(func() Type[string, string, string] {
return MakeType(
"Default",
Is[string](),
func(s string) Decode[Context, string] {
return func(c Context) Validation[string] {
return validation.Success("default")
}
},
F.Identity[string],
)
})
primary := MakeType(
"Primary",
Is[string](),
func(s string) Decode[Context, string] {
return func(c Context) Validation[string] {
if s == "primary" {
return validation.Success("from primary")
}
return validation.FailureWithMessage[string](s, "not primary")(c)
}
},
F.Identity[string],
)
secondary := MakeType(
"Secondary",
Is[string](),
func(s string) Decode[Context, string] {
return func(c Context) Validation[string] {
if s == "secondary" {
return validation.Success("from secondary")
}
return validation.FailureWithMessage[string](s, "not secondary")(c)
}
},
F.Identity[string],
)
// Chain: try primary, then secondary, then default
combined := m.Concat(m.Concat(primary, secondary), m.Empty())
t.Run("uses primary when it succeeds", func(t *testing.T) {
result := combined.Decode("primary")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, string](""))(result)
assert.Equal(t, "from primary", value)
})
t.Run("uses secondary when primary fails", func(t *testing.T) {
result := combined.Decode("secondary")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, string](""))(result)
assert.Equal(t, "from secondary", value)
})
t.Run("uses default when both fail", func(t *testing.T) {
result := combined.Decode("other")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, string](""))(result)
assert.Equal(t, "default", value)
})
})
t.Run("satisfies monoid laws", func(t *testing.T) {
m := AltMonoid(func() Type[int, string, string] {
return MakeType(
"DefaultZero",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(0)
}
},
strconv.Itoa,
)
})
codec1 := MakeType(
"Codec1",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(10)
}
},
strconv.Itoa,
)
codec2 := MakeType(
"Codec2",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(20)
}
},
strconv.Itoa,
)
codec3 := MakeType(
"Codec3",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(30)
}
},
strconv.Itoa,
)
t.Run("left identity", func(t *testing.T) {
// m.Concat(m.Empty(), codec) should behave like codec
// But with AltMonoid, if codec fails, it falls back to empty
combined := m.Concat(m.Empty(), codec1)
result := combined.Decode("input")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](-1))(result)
// Empty (0) comes first, so it wins
assert.Equal(t, 0, value)
})
t.Run("right identity", func(t *testing.T) {
// m.Concat(codec, m.Empty()) tries codec first, falls back to empty
combined := m.Concat(codec1, m.Empty())
result := combined.Decode("input")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](-1))(result)
assert.Equal(t, 10, value, "codec1 should win")
})
t.Run("associativity", func(t *testing.T) {
// For AltMonoid, first success wins
left := m.Concat(m.Concat(codec1, codec2), codec3)
right := m.Concat(codec1, m.Concat(codec2, codec3))
resultLeft := left.Decode("input")
resultRight := right.Decode("input")
assert.True(t, either.IsRight(resultLeft))
assert.True(t, either.IsRight(resultRight))
valueLeft := either.GetOrElse(reader.Of[validation.Errors, int](-1))(resultLeft)
valueRight := either.GetOrElse(reader.Of[validation.Errors, int](-1))(resultRight)
// Both should return 10 (first success)
assert.Equal(t, valueLeft, valueRight)
assert.Equal(t, 10, valueLeft)
})
})
t.Run("encoding uses first codec", func(t *testing.T) {
m := AltMonoid(func() Type[int, string, string] {
return MakeType(
"Default",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(0)
}
},
func(n int) string { return "DEFAULT" },
)
})
codec1 := MakeType(
"Codec1",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(42)
}
},
func(n int) string { return fmt.Sprintf("FIRST:%d", n) },
)
codec2 := MakeType(
"Codec2",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(100)
}
},
func(n int) string { return fmt.Sprintf("SECOND:%d", n) },
)
combined := m.Concat(codec1, codec2)
// Encoding should use first codec's encoder
encoded := combined.Encode(42)
assert.Equal(t, "FIRST:42", encoded)
})
}

View File

@@ -4,6 +4,7 @@ import (
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/internal/formatting"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/optics/codec/decode"
"github.com/IBM/fp-go/v2/optics/codec/validate"
"github.com/IBM/fp-go/v2/optics/codec/validation"
@@ -40,6 +41,27 @@ type (
// Codec combines a Decoder and an Encoder for bidirectional transformations.
// It can decode input I to type A and encode type A to output O.
//
// This is a simple struct that pairs a decoder with an encoder, providing
// the basic building blocks for bidirectional data transformation. Unlike
// the Type interface, Codec is a concrete struct without validation context
// or type checking capabilities.
//
// Type Parameters:
// - I: The input type to decode from
// - O: The output type to encode to
// - A: The intermediate type (decoded to, encoded from)
//
// Fields:
// - Decode: A decoder that transforms I to A
// - Encode: An encoder that transforms A to O
//
// Example:
// A Codec[string, string, int] can decode strings to integers and
// encode integers back to strings.
//
// Note: For most use cases, prefer using the Type interface which provides
// additional validation and type checking capabilities.
Codec[I, O, A any] struct {
Decode decoder.Decoder[I, A]
Encode encoder.Encoder[O, A]
@@ -55,16 +77,82 @@ type (
// Validate is a function that validates input I to produce type A.
// It takes an input and returns a Reader that depends on the validation Context.
//
// The Validate type is the core validation abstraction, defined as:
// Reader[I, Decode[Context, A]]
//
// This means:
// 1. It takes an input of type I
// 2. Returns a Reader that depends on validation Context
// 3. That Reader produces a Validation[A] (Either[Errors, A])
//
// This layered structure allows validators to:
// - Access the input value
// - Track validation context (path in nested structures)
// - Accumulate multiple validation errors
// - Compose with other validators
//
// Example:
// A Validate[string, int] takes a string and returns a context-aware
// function that validates and converts it to an integer.
Validate[I, A any] = validate.Validate[I, A]
// Decode is a function that decodes input I to type A with validation.
// It returns a Validation result directly.
//
// The Decode type is defined as:
// Reader[I, Validation[A]]
//
// This is simpler than Validate as it doesn't require explicit context passing.
// The context is typically created automatically when the decoder is invoked.
//
// Decode is used when:
// - You don't need to manually manage validation context
// - You want a simpler API for basic validation
// - You're working at the top level of validation
//
// Example:
// A Decode[string, int] takes a string and returns a Validation[int]
// which is Either[Errors, int].
Decode[I, A any] = decode.Decode[I, A]
// Encode is a function that encodes type A to output O.
//
// Encode is simply a Reader[A, O], which is a function from A to O.
// Encoders are pure functions with no error handling - they assume
// the input is valid.
//
// Encoding is the inverse of decoding:
// - Decoding: I -> Validation[A] (may fail)
// - Encoding: A -> O (always succeeds)
//
// Example:
// An Encode[int, string] takes an integer and returns its string
// representation.
Encode[A, O any] = Reader[A, O]
// Decoder is an interface for types that can decode and validate input.
//
// A Decoder transforms input of type I into a validated value of type A,
// providing detailed error information when validation fails. It supports
// both context-aware validation (via Validate) and direct decoding (via Decode).
//
// Type Parameters:
// - I: The input type to decode from
// - A: The target type to decode to
//
// Methods:
// - Name(): Returns a descriptive name for this decoder (used in error messages)
// - Validate(I): Returns a context-aware validation function that can track
// the path through nested structures
// - Decode(I): Directly decodes input to a Validation result with a fresh context
//
// The Validate method is more flexible as it returns a Reader that can be called
// with different contexts, while Decode is a convenience method that creates a
// new context automatically.
//
// Example:
// A Decoder[string, int] can decode strings to integers with validation.
Decoder[I, A any] interface {
Name() string
Validate(I) Decode[Context, A]
@@ -72,13 +160,76 @@ type (
}
// Encoder is an interface for types that can encode values.
//
// An Encoder transforms values of type A into output format O. This is the
// inverse operation of decoding, allowing bidirectional transformations.
//
// Type Parameters:
// - A: The source type to encode from
// - O: The output type to encode to
//
// Methods:
// - Encode(A): Transforms a value of type A into output format O
//
// Encoders are pure functions with no validation or error handling - they
// assume the input is valid. Validation should be performed during decoding.
//
// Example:
// An Encoder[int, string] can encode integers to their string representation.
Encoder[A, O any] interface {
// Encode transforms a value of type A into output format O.
Encode(A) O
}
// Type is a bidirectional codec that combines encoding, decoding, validation,
// and type checking capabilities. It represents a complete specification of
// how to work with a particular type.
//
// Type is the central abstraction in the codec package, providing:
// - Decoding: Transform input I to validated type A
// - Encoding: Transform type A to output O
// - Validation: Context-aware validation with detailed error reporting
// - Type Checking: Runtime type verification via Is()
// - Formatting: Human-readable type descriptions via Name()
//
// Type Parameters:
// - A: The target type (what we decode to and encode from)
// - O: The output type (what we encode to)
// - I: The input type (what we decode from)
//
// Common patterns:
// - Type[A, A, A]: Identity codec (no transformation)
// - Type[A, string, string]: String-based serialization
// - Type[A, any, any]: Generic codec accepting any input/output
// - Type[A, JSON, JSON]: JSON codec
//
// Methods:
// - Name(): Returns the codec's descriptive name
// - Validate(I): Returns context-aware validation function
// - Decode(I): Decodes input with automatic context creation
// - Encode(A): Encodes value to output format
// - AsDecoder(): Returns this Type as a Decoder interface
// - AsEncoder(): Returns this Type as an Encoder interface
// - Is(any): Checks if a value can be converted to type A
//
// Example usage:
// intCodec := codec.Int() // Type[int, int, any]
// stringCodec := codec.String() // Type[string, string, any]
// intFromString := codec.IntFromString() // Type[int, string, string]
//
// // Decode
// result := intFromString.Decode("42") // Validation[int]
//
// // Encode
// str := intFromString.Encode(42) // "42"
//
// // Type check
// isInt := intCodec.Is(42) // Right(42)
// notInt := intCodec.Is("42") // Left(error)
//
// Composition:
// Types can be composed using operators like Alt, Map, Chain, and Pipe
// to build complex codecs from simpler ones.
Type[A, O, I any] interface {
Formattable
Decoder[I, A]
@@ -99,9 +250,92 @@ type (
// contain a value of type A. It provides a way to preview and review values.
Prism[S, A any] = prism.Prism[S, A]
// Refinement represents the concept that B is a specialized type of A
// Refinement represents the concept that B is a specialized type of A.
// It's an alias for Prism[A, B], providing a semantic name for type refinement operations.
//
// A refinement allows you to:
// - Preview: Try to extract a B from an A (may fail if A is not a B)
// - Review: Inject a B back into an A
//
// This is useful for working with subtypes, validated types, or constrained types.
//
// Example:
// - Refinement[int, PositiveInt] - refines int to positive integers only
// - Refinement[string, NonEmptyString] - refines string to non-empty strings
// - Refinement[any, User] - refines any to User type
Refinement[A, B any] = Prism[A, B]
Kleisli[A, B, O, I any] = Reader[A, Type[B, O, I]]
// Kleisli represents a Kleisli arrow in the codec context.
// It's a function that takes a value of type A and returns a codec Type[B, O, I].
//
// This is the fundamental building block for codec transformations and compositions.
// Kleisli arrows allow you to:
// - Chain codec operations
// - Build dependent codecs (where the next codec depends on the previous result)
// - Create codec pipelines
//
// Type Parameters:
// - A: The input type to the function
// - B: The target type that the resulting codec decodes to
// - O: The output type that the resulting codec encodes to
// - I: The input type that the resulting codec decodes from
//
// Example:
// A Kleisli[string, int, string, string] takes a string and returns a codec
// that can decode strings to ints and encode ints to strings.
Kleisli[A, B, O, I any] = Reader[A, Type[B, O, I]]
// Operator is a specialized Kleisli arrow that transforms codecs.
// It takes a codec Type[A, O, I] and returns a new codec Type[B, O, I].
//
// Operators are the primary way to build codec transformation pipelines.
// They enable functional composition of codec transformations using F.Pipe.
//
// Type Parameters:
// - A: The source type that the input codec decodes to
// - B: The target type that the output codec decodes to
// - O: The output type (same for both input and output codecs)
// - I: The input type (same for both input and output codecs)
//
// Common operators include:
// - Map: Transforms the decoded value
// - Chain: Sequences dependent codec operations
// - Alt: Provides alternative fallback codecs
// - Refine: Adds validation constraints
//
// Example:
// An Operator[int, PositiveInt, int, any] transforms a codec that decodes
// to int into a codec that decodes to PositiveInt (with validation).
//
// Usage with F.Pipe:
// codec := F.Pipe2(
// baseCodec,
// operator1, // Operator[A, B, O, I]
// operator2, // Operator[B, C, O, I]
// )
Operator[A, B, O, I any] = Kleisli[Type[A, O, I], B, O, I]
// Monoid represents an algebraic structure with an associative binary operation
// and an identity element.
//
// A Monoid[A] provides:
// - Empty(): Returns the identity element
// - Concat(A, A): Combines two values associatively
//
// Monoid laws:
// 1. Left Identity: Concat(Empty(), a) = a
// 2. Right Identity: Concat(a, Empty()) = a
// 3. Associativity: Concat(Concat(a, b), c) = Concat(a, Concat(b, c))
//
// In the codec context, monoids are used to:
// - Combine multiple codecs with specific semantics
// - Build codec chains with fallback behavior (AltMonoid)
// - Aggregate validation results (ApplicativeMonoid)
// - Compose codec transformations
//
// Example monoids for codecs:
// - AltMonoid: First success wins (alternative semantics)
// - ApplicativeMonoid: Combines successful results using inner monoid
// - AlternativeMonoid: Combines applicative and alternative behaviors
Monoid[A any] = monoid.Monoid[A]
)

View File

@@ -0,0 +1,153 @@
// 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 prism
import (
"fmt"
F "github.com/IBM/fp-go/v2/function"
P "github.com/IBM/fp-go/v2/optics/prism"
)
// Compose creates a Kleisli arrow that composes a prism with an isomorphism.
//
// This function takes a Prism[A, B] and returns a Kleisli arrow that can transform
// any Iso[S, A] into a Prism[S, B]. The resulting prism changes the source type from
// A to S using the bidirectional transformation provided by the isomorphism, while
// maintaining the same focus type B.
//
// The composition works as follows:
// - GetOption: First transforms S to A using the iso's Get, then extracts B from A using the prism's GetOption
// - ReverseGet: First constructs A from B using the prism's ReverseGet, then transforms A to S using the iso's ReverseGet
//
// This is the dual operation of optics/prism/iso.Compose:
// - optics/prism/iso.Compose: Transforms the focus type (A → B) while keeping source type (S) constant
// - optics/iso/prism.Compose: Transforms the source type (A → S) while keeping focus type (B) constant
//
// This is particularly useful when you have a prism that works with one type but you
// need to adapt it to work with a different source type that has a lossless bidirectional
// transformation to the original type.
//
// Type Parameters:
// - S: The new source type after applying the isomorphism
// - A: The original source type of the prism
// - B: The focus type (remains constant through composition)
//
// Parameters:
// - ab: A prism that extracts B from A
//
// Returns:
// - A Kleisli arrow (function) that takes an Iso[S, A] and returns a Prism[S, B]
//
// Laws:
// The composed prism must satisfy the prism laws:
// 1. GetOption(ReverseGet(b)) == Some(b) for all b: B
// 2. If GetOption(s) == Some(a), then GetOption(ReverseGet(a)) == Some(a)
//
// These laws are preserved because:
// - The isomorphism satisfies: ia.ReverseGet(ia.Get(s)) == s and ia.Get(ia.ReverseGet(a)) == a
// - The original prism satisfies the prism laws
//
// Haskell Equivalent:
// This corresponds to the (.) operator for composing optics in Haskell's lens library,
// specifically when composing an Iso with a Prism:
//
// iso . prism :: Iso s a -> Prism a b -> Prism s b
//
// In Haskell's lens library, this is part of the general optic composition mechanism.
// See: https://hackage.haskell.org/package/lens/docs/Control-Lens-Iso.html
//
// Example - Composing with Either prism:
//
// import (
// "github.com/IBM/fp-go/v2/either"
// "github.com/IBM/fp-go/v2/optics/iso"
// "github.com/IBM/fp-go/v2/optics/prism"
// IP "github.com/IBM/fp-go/v2/optics/iso/prism"
// O "github.com/IBM/fp-go/v2/option"
// )
//
// // Create a prism that extracts Right values from Either[error, string]
// rightPrism := prism.FromEither[error, string]()
//
// // Create an isomorphism between []byte and string
// bytesStringIso := iso.MakeIso(
// func(b []byte) string { return string(b) },
// func(s string) []byte { return []byte(s) },
// )
//
// // Compose them to get a prism that works with []byte as source
// bytesPrism := IP.Compose(rightPrism)(bytesStringIso)
//
// // Use the composed prism
// // First converts []byte to string via iso, then extracts Right value
// bytes := []byte("hello")
// either := either.Right[error](string(bytes))
// result := bytesPrism.GetOption(bytes) // Extracts "hello" if Right
//
// // Construct []byte from string
// constructed := bytesPrism.ReverseGet("world")
// // Returns []byte("world") wrapped in Right
//
// Example - Composing with custom types:
//
// type JSON []byte
// type Config struct {
// Host string
// Port int
// }
//
// // Isomorphism between JSON and []byte
// jsonIso := iso.MakeIso(
// func(j JSON) []byte { return []byte(j) },
// func(b []byte) JSON { return JSON(b) },
// )
//
// // Prism that extracts Config from []byte (via JSON parsing)
// configPrism := prism.MakePrism(
// func(b []byte) option.Option[Config] {
// var cfg Config
// if err := json.Unmarshal(b, &cfg); err != nil {
// return option.None[Config]()
// }
// return option.Some(cfg)
// },
// func(cfg Config) []byte {
// b, _ := json.Marshal(cfg)
// return b
// },
// )
//
// // Compose to work with JSON type instead of []byte
// jsonConfigPrism := IP.Compose(configPrism)(jsonIso)
//
// jsonData := JSON(`{"host":"localhost","port":8080}`)
// config := jsonConfigPrism.GetOption(jsonData)
// // config is Some(Config{Host: "localhost", Port: 8080})
//
// See also:
// - github.com/IBM/fp-go/v2/optics/iso for isomorphism operations
// - github.com/IBM/fp-go/v2/optics/prism for prism operations
// - github.com/IBM/fp-go/v2/optics/prism/iso for the dual composition (transforming focus type)
func Compose[S, A, B any](ab Prism[A, B]) P.Kleisli[S, Iso[S, A], B] {
return func(ia Iso[S, A]) Prism[S, B] {
return P.MakePrismWithName(
F.Flow2(ia.Get, ab.GetOption),
F.Flow2(ab.ReverseGet, ia.ReverseGet),
fmt.Sprintf("IsoCompose[%s -> %s]", ia, ab),
)
}
}

View File

@@ -0,0 +1,435 @@
// 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 prism
import (
"encoding/json"
"strconv"
"testing"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
I "github.com/IBM/fp-go/v2/optics/iso"
P "github.com/IBM/fp-go/v2/optics/prism"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestComposeWithEitherPrism tests composing a prism with an isomorphism using Either
func TestComposeWithEitherPrism(t *testing.T) {
// Create a prism that extracts Right values from Either[error, string]
rightPrism := P.FromEither[error, string]()
// Create an isomorphism between []byte and Either[error, string]
bytesEitherIso := I.MakeIso(
func(b []byte) E.Either[error, string] {
return E.Right[error](string(b))
},
func(e E.Either[error, string]) []byte {
return []byte(E.GetOrElse(func(error) string { return "" })(e))
},
)
// Compose them: Prism[Either, string] with Iso[[]byte, Either] -> Prism[[]byte, string]
bytesPrism := Compose[[]byte](rightPrism)(bytesEitherIso)
t.Run("GetOption extracts string from []byte", func(t *testing.T) {
bytes := []byte("hello")
result := bytesPrism.GetOption(bytes)
assert.True(t, O.IsSome(result))
str := O.GetOrElse(F.Constant(""))(result)
assert.Equal(t, "hello", str)
})
t.Run("ReverseGet constructs []byte from string", func(t *testing.T) {
value := "world"
result := bytesPrism.ReverseGet(value)
assert.Equal(t, []byte("world"), result)
})
t.Run("Round-trip through GetOption and ReverseGet", func(t *testing.T) {
original := "test"
// ReverseGet to create []byte
bytes := bytesPrism.ReverseGet(original)
// GetOption to extract string back
result := bytesPrism.GetOption(bytes)
assert.True(t, O.IsSome(result))
extracted := O.GetOrElse(F.Constant(""))(result)
assert.Equal(t, original, extracted)
})
}
// TestComposeWithOptionPrism tests composing a prism with an isomorphism using Option
func TestComposeWithOptionPrism(t *testing.T) {
// Create a prism that extracts Some values from Option[int]
somePrism := P.FromOption[int]()
// Create an isomorphism between string and Option[int]
stringOptionIso := I.MakeIso(
func(s string) O.Option[int] {
i, err := strconv.Atoi(s)
if err != nil {
return O.None[int]()
}
return O.Some(i)
},
func(opt O.Option[int]) string {
return strconv.Itoa(O.GetOrElse(F.Constant(0))(opt))
},
)
// Compose them: Prism[Option, int] with Iso[string, Option] -> Prism[string, int]
stringPrism := Compose[string](somePrism)(stringOptionIso)
t.Run("GetOption extracts int from valid string", func(t *testing.T) {
result := stringPrism.GetOption("42")
assert.True(t, O.IsSome(result))
num := O.GetOrElse(F.Constant(0))(result)
assert.Equal(t, 42, num)
})
t.Run("GetOption returns None for invalid string", func(t *testing.T) {
result := stringPrism.GetOption("invalid")
assert.True(t, O.IsNone(result))
})
t.Run("ReverseGet constructs string from int", func(t *testing.T) {
result := stringPrism.ReverseGet(100)
assert.Equal(t, "100", result)
})
}
// Custom types for testing
type Celsius float64
type Fahrenheit float64
type Temperature interface {
isTemperature()
}
type CelsiusTemp struct {
Value Celsius
}
func (c CelsiusTemp) isTemperature() {}
type FahrenheitTemp struct {
Value Fahrenheit
}
func (f FahrenheitTemp) isTemperature() {}
// TestComposeWithCustomPrism tests composing with custom types
func TestComposeWithCustomPrism(t *testing.T) {
// Prism that extracts Celsius from Temperature
celsiusPrism := P.MakePrism(
func(t Temperature) O.Option[Celsius] {
if ct, ok := t.(CelsiusTemp); ok {
return O.Some(ct.Value)
}
return O.None[Celsius]()
},
func(c Celsius) Temperature {
return CelsiusTemp{Value: c}
},
)
// Isomorphism between Fahrenheit and Temperature
fahrenheitTempIso := I.MakeIso(
func(f Fahrenheit) Temperature {
celsius := Celsius((f - 32) * 5 / 9)
return CelsiusTemp{Value: celsius}
},
func(t Temperature) Fahrenheit {
if ct, ok := t.(CelsiusTemp); ok {
return Fahrenheit(ct.Value*9/5 + 32)
}
return 0
},
)
// Compose: Prism[Temperature, Celsius] with Iso[Fahrenheit, Temperature] -> Prism[Fahrenheit, Celsius]
fahrenheitPrism := Compose[Fahrenheit](celsiusPrism)(fahrenheitTempIso)
t.Run("GetOption extracts Celsius from Fahrenheit", func(t *testing.T) {
fahrenheit := Fahrenheit(68)
result := fahrenheitPrism.GetOption(fahrenheit)
assert.True(t, O.IsSome(result))
celsius := O.GetOrElse(F.Constant(Celsius(0)))(result)
assert.InDelta(t, 20.0, float64(celsius), 0.01)
})
t.Run("ReverseGet constructs Fahrenheit from Celsius", func(t *testing.T) {
celsius := Celsius(20)
result := fahrenheitPrism.ReverseGet(celsius)
assert.InDelta(t, 68.0, float64(result), 0.01)
})
t.Run("Round-trip preserves value", func(t *testing.T) {
original := Celsius(25)
// ReverseGet to create Fahrenheit
fahrenheit := fahrenheitPrism.ReverseGet(original)
// GetOption to extract Celsius back
result := fahrenheitPrism.GetOption(fahrenheit)
assert.True(t, O.IsSome(result))
extracted := O.GetOrElse(F.Constant(Celsius(0)))(result)
assert.InDelta(t, float64(original), float64(extracted), 0.01)
})
}
// TestComposeIdentityIso tests composing with an identity isomorphism
func TestComposeIdentityIso(t *testing.T) {
// Prism that extracts Right values
rightPrism := P.FromEither[error, string]()
// Identity isomorphism on Either
idIso := I.Id[E.Either[error, string]]()
// Compose with identity should not change behavior
composedPrism := Compose[E.Either[error, string]](rightPrism)(idIso)
t.Run("Composed prism behaves like original prism", func(t *testing.T) {
either := E.Right[error]("test")
// Original prism
originalResult := rightPrism.GetOption(either)
// Composed prism
composedResult := composedPrism.GetOption(either)
assert.Equal(t, originalResult, composedResult)
})
t.Run("ReverseGet produces same result", func(t *testing.T) {
value := "test"
// Original prism
originalResult := rightPrism.ReverseGet(value)
// Composed prism
composedResult := composedPrism.ReverseGet(value)
assert.Equal(t, originalResult, composedResult)
})
}
// TestComposeChaining tests chaining multiple compositions
func TestComposeChaining(t *testing.T) {
// Prism: extracts Right values from Either[error, int]
rightPrism := P.FromEither[error, int]()
// Iso 1: string to Either[error, int]
stringEitherIso := I.MakeIso(
func(s string) E.Either[error, int] {
i, err := strconv.Atoi(s)
if err != nil {
return E.Left[int](err)
}
return E.Right[error](i)
},
func(e E.Either[error, int]) string {
return strconv.Itoa(E.GetOrElse(func(error) int { return 0 })(e))
},
)
// Iso 2: []byte to string
bytesStringIso := I.MakeIso(
func(b []byte) string { return string(b) },
func(s string) []byte { return []byte(s) },
)
// First composition: Prism[Either, int] with Iso[string, Either] -> Prism[string, int]
step1 := Compose[string](rightPrism)(stringEitherIso)
// Second composition: Prism[string, int] with Iso[[]byte, string] -> Prism[[]byte, int]
step2 := Compose[[]byte](step1)(bytesStringIso)
t.Run("Chained composition extracts correctly", func(t *testing.T) {
bytes := []byte("42")
result := step2.GetOption(bytes)
assert.True(t, O.IsSome(result))
num := O.GetOrElse(F.Constant(0))(result)
assert.Equal(t, 42, num)
})
t.Run("Chained composition ReverseGet works correctly", func(t *testing.T) {
num := 100
result := step2.ReverseGet(num)
assert.Equal(t, []byte("100"), result)
})
}
// TestComposePrismLaws verifies that the composed prism satisfies prism laws
func TestComposePrismLaws(t *testing.T) {
// Create a prism
prism := P.FromEither[error, int]()
// Create an isomorphism from string to Either[error, int]
iso := I.MakeIso(
func(s string) E.Either[error, int] {
i, err := strconv.Atoi(s)
if err != nil {
return E.Left[int](err)
}
return E.Right[error](i)
},
func(e E.Either[error, int]) string {
return strconv.Itoa(E.GetOrElse(func(error) int { return 0 })(e))
},
)
// Compose them
composed := Compose[string](prism)(iso)
t.Run("Law 1: GetOption(ReverseGet(b)) == Some(b)", func(t *testing.T) {
value := 42
// ReverseGet then GetOption should return Some(value)
source := composed.ReverseGet(value)
result := composed.GetOption(source)
assert.True(t, O.IsSome(result))
extracted := O.GetOrElse(F.Constant(0))(result)
assert.Equal(t, value, extracted)
})
t.Run("Law 2: If GetOption(s) == Some(a), then GetOption(ReverseGet(a)) == Some(a)", func(t *testing.T) {
source := "100"
// First GetOption
firstResult := composed.GetOption(source)
assert.True(t, O.IsSome(firstResult))
// Extract the value
value := O.GetOrElse(F.Constant(0))(firstResult)
// ReverseGet then GetOption again
reconstructed := composed.ReverseGet(value)
secondResult := composed.GetOption(reconstructed)
assert.True(t, O.IsSome(secondResult))
finalValue := O.GetOrElse(F.Constant(0))(secondResult)
assert.Equal(t, value, finalValue)
})
}
// TestComposeWithJSON tests a practical example with JSON parsing
func TestComposeWithJSON(t *testing.T) {
type Config struct {
Host string `json:"host"`
Port int `json:"port"`
}
// Prism that extracts Config from []byte (via JSON parsing)
configPrism := P.MakePrism(
func(b []byte) O.Option[Config] {
var cfg Config
if err := json.Unmarshal(b, &cfg); err != nil {
return O.None[Config]()
}
return O.Some(cfg)
},
func(cfg Config) []byte {
b, _ := json.Marshal(cfg)
return b
},
)
// Isomorphism between string and []byte
stringBytesIso := I.MakeIso(
func(s string) []byte { return []byte(s) },
func(b []byte) string { return string(b) },
)
// Compose: Prism[[]byte, Config] with Iso[string, []byte] -> Prism[string, Config]
stringConfigPrism := Compose[string](configPrism)(stringBytesIso)
t.Run("GetOption parses valid JSON string", func(t *testing.T) {
jsonStr := `{"host":"localhost","port":8080}`
result := stringConfigPrism.GetOption(jsonStr)
assert.True(t, O.IsSome(result))
cfg := O.GetOrElse(F.Constant(Config{}))(result)
assert.Equal(t, "localhost", cfg.Host)
assert.Equal(t, 8080, cfg.Port)
})
t.Run("GetOption returns None for invalid JSON", func(t *testing.T) {
invalidJSON := `{invalid json}`
result := stringConfigPrism.GetOption(invalidJSON)
assert.True(t, O.IsNone(result))
})
t.Run("ReverseGet creates JSON string from Config", func(t *testing.T) {
cfg := Config{Host: "example.com", Port: 443}
result := stringConfigPrism.ReverseGet(cfg)
// Parse it back to verify
var parsed Config
err := json.Unmarshal([]byte(result), &parsed)
assert.NoError(t, err)
assert.Equal(t, cfg, parsed)
})
}
// TestComposeWithEmptyValues tests edge cases with empty/zero values
func TestComposeWithEmptyValues(t *testing.T) {
// Prism that extracts Right values
prism := P.FromEither[error, []byte]()
// Isomorphism between string and Either[error, []byte]
iso := I.MakeIso(
func(s string) E.Either[error, []byte] {
return E.Right[error]([]byte(s))
},
func(e E.Either[error, []byte]) string {
return string(E.GetOrElse(func(error) []byte { return []byte{} })(e))
},
)
composed := Compose[string](prism)(iso)
t.Run("Empty string is handled correctly", func(t *testing.T) {
result := composed.GetOption("")
assert.True(t, O.IsSome(result))
bytes := O.GetOrElse(F.Constant([]byte("default")))(result)
assert.Equal(t, []byte{}, bytes)
})
t.Run("ReverseGet with empty bytes", func(t *testing.T) {
bytes := []byte{}
result := composed.ReverseGet(bytes)
assert.Equal(t, "", result)
})
}

View File

@@ -0,0 +1,99 @@
// 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 prism provides utilities for composing prisms with isomorphisms.
//
// This package enables the composition of prisms (optics for sum types) with
// isomorphisms (bidirectional transformations), allowing you to transform the
// source type of a prism using an isomorphism. This is the inverse operation
// of optics/prism/iso, where we transform the focus type instead of the source type.
//
// # Key Concepts
//
// A Prism[S, A] is an optic that focuses on a specific variant within a sum type S,
// extracting values of type A. An Iso[S, A] represents a bidirectional transformation
// between types S and A without loss of information.
//
// When you compose a Prism[A, B] with an Iso[S, A], you get a Prism[S, B] that:
// - Transforms S to A using the isomorphism's Get
// - Extracts values of type B from A (using the prism)
// - Can construct S from B by first using the prism's ReverseGet to get A, then the iso's ReverseGet
//
// # Example Usage
//
// import (
// "github.com/IBM/fp-go/v2/optics/iso"
// "github.com/IBM/fp-go/v2/optics/prism"
// IP "github.com/IBM/fp-go/v2/optics/iso/prism"
// O "github.com/IBM/fp-go/v2/option"
// )
//
// // Create an isomorphism between []byte and string
// bytesStringIso := iso.MakeIso(
// func(b []byte) string { return string(b) },
// func(s string) []byte { return []byte(s) },
// )
//
// // Create a prism that extracts Right values from Either[error, string]
// rightPrism := prism.FromEither[error, string]()
//
// // Compose them to get a prism that works with []byte as source
// bytesPrism := IP.Compose(rightPrism)(bytesStringIso)
//
// // Use the composed prism
// bytes := []byte(`{"status":"ok"}`)
// // First converts bytes to string via iso, then extracts Right value
// result := bytesPrism.GetOption(either.Right[error](string(bytes)))
//
// # Comparison with optics/prism/iso
//
// This package (optics/iso/prism) is the dual of optics/prism/iso:
// - optics/prism/iso: Composes Iso[A, B] with Prism[S, A] → Prism[S, B] (transforms focus type)
// - optics/iso/prism: Composes Prism[A, B] with Iso[S, A] → Prism[S, B] (transforms source type)
//
// # Type Aliases
//
// This package re-exports key types from the iso and prism packages for convenience:
// - Iso[S, A]: An isomorphism between types S and A
// - Prism[S, A]: A prism focusing on type A within sum type S
package prism
import (
I "github.com/IBM/fp-go/v2/optics/iso"
P "github.com/IBM/fp-go/v2/optics/prism"
)
type (
// Iso represents an isomorphism between types S and A.
// It is a bidirectional transformation that converts between two types
// without any loss of information.
//
// Type Parameters:
// - S: The source type
// - A: The target type
//
// See github.com/IBM/fp-go/v2/optics/iso for the full Iso API.
Iso[S, A any] = I.Iso[S, A]
// Prism is an optic used to select part of a sum type (tagged union).
// It provides operations to extract and construct values within sum types.
//
// Type Parameters:
// - S: The source type (sum type)
// - A: The focus type (variant within the sum type)
//
// See github.com/IBM/fp-go/v2/optics/prism for the full Prism API.
Prism[S, A any] = P.Prism[S, A]
)

View File

@@ -0,0 +1,156 @@
// 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 iso
import (
"fmt"
F "github.com/IBM/fp-go/v2/function"
P "github.com/IBM/fp-go/v2/optics/prism"
O "github.com/IBM/fp-go/v2/option"
)
// Compose creates an operator that composes an isomorphism with a prism.
//
// This function takes an isomorphism Iso[A, B] and returns an operator that can
// transform any Prism[S, A] into a Prism[S, B]. The resulting prism maintains
// the same source type S but changes the focus type from A to B using the
// bidirectional transformation provided by the isomorphism.
//
// The composition works as follows:
// - GetOption: First extracts A from S using the prism, then transforms A to B using the iso's Get
// - ReverseGet: First transforms B to A using the iso's ReverseGet, then constructs S using the prism's ReverseGet
//
// This is particularly useful when you have a prism that focuses on one type but
// you need to work with a different type that has a lossless bidirectional
// transformation to the original type.
//
// Haskell Equivalent:
// This corresponds to the (.) operator for composing optics in Haskell's lens library,
// specifically when composing a Prism with an Iso:
//
// prism . iso :: Prism s a -> Iso a b -> Prism s b
//
// In Haskell's lens library, this is part of the general optic composition mechanism.
// See: https://hackage.haskell.org/package/lens/docs/Control-Lens-Prism.html
//
// Type Parameters:
// - S: The source type (sum type) that the prism operates on
// - A: The original focus type of the prism
// - B: The new focus type after applying the isomorphism
//
// Parameters:
// - ab: An isomorphism between types A and B that defines the bidirectional transformation
//
// Returns:
// - An Operator[S, A, B] that transforms Prism[S, A] into Prism[S, B]
//
// Laws:
// The composed prism must satisfy the prism laws:
// 1. GetOption(ReverseGet(b)) == Some(b) for all b: B
// 2. If GetOption(s) == Some(a), then GetOption(ReverseGet(a)) == Some(a)
//
// These laws are preserved because:
// - The isomorphism satisfies: ab.ReverseGet(ab.Get(a)) == a and ab.Get(ab.ReverseGet(b)) == b
// - The original prism satisfies the prism laws
//
// Example - Composing string/bytes isomorphism with Either prism:
//
// import (
// "github.com/IBM/fp-go/v2/either"
// "github.com/IBM/fp-go/v2/optics/iso"
// "github.com/IBM/fp-go/v2/optics/prism"
// PI "github.com/IBM/fp-go/v2/optics/prism/iso"
// O "github.com/IBM/fp-go/v2/option"
// )
//
// // Create an isomorphism between string and []byte
// stringBytesIso := iso.MakeIso(
// func(s string) []byte { return []byte(s) },
// func(b []byte) string { return string(b) },
// )
//
// // Create a prism that extracts Right values from Either[error, string]
// rightPrism := prism.FromEither[error, string]()
//
// // Compose them to get a prism that works with []byte instead of string
// bytesPrism := PI.Compose(stringBytesIso)(rightPrism)
//
// // Extract bytes from a Right value
// success := either.Right[error]("hello")
// result := bytesPrism.GetOption(success)
// // result is Some([]byte("hello"))
//
// // Extract from a Left value returns None
// failure := either.Left[string](errors.New("error"))
// result = bytesPrism.GetOption(failure)
// // result is None
//
// // Construct an Either from bytes
// constructed := bytesPrism.ReverseGet([]byte("world"))
// // constructed is Right("world")
//
// Example - Composing with custom types:
//
// type Celsius float64
// type Fahrenheit float64
//
// // Isomorphism between Celsius and Fahrenheit
// tempIso := iso.MakeIso(
// func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
// func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
// )
//
// // Prism that extracts temperature from a weather report
// type WeatherReport struct {
// Temperature Celsius
// Condition string
// }
// tempPrism := prism.MakePrism(
// func(w WeatherReport) option.Option[Celsius] {
// return option.Some(w.Temperature)
// },
// func(c Celsius) WeatherReport {
// return WeatherReport{Temperature: c}
// },
// )
//
// // Compose to work with Fahrenheit instead
// fahrenheitPrism := PI.Compose(tempIso)(tempPrism)
//
// report := WeatherReport{Temperature: 20, Condition: "sunny"}
// temp := fahrenheitPrism.GetOption(report)
// // temp is Some(68.0) in Fahrenheit
//
// See also:
// - github.com/IBM/fp-go/v2/optics/iso for isomorphism operations
// - github.com/IBM/fp-go/v2/optics/prism for prism operations
// - Operator for the type signature of the returned function
func Compose[S, A, B any](ab Iso[A, B]) Operator[S, A, B] {
return func(pa Prism[S, A]) Prism[S, B] {
return P.MakePrismWithName(
F.Flow2(
pa.GetOption,
O.Map(ab.Get),
),
F.Flow2(
ab.ReverseGet,
pa.ReverseGet,
),
fmt.Sprintf("PrismCompose[%s -> %s]", pa, ab),
)
}
}

View File

@@ -0,0 +1,369 @@
// 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 iso
import (
"errors"
"strconv"
"testing"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
I "github.com/IBM/fp-go/v2/optics/iso"
P "github.com/IBM/fp-go/v2/optics/prism"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestComposeWithEitherPrism tests composing an isomorphism with an Either prism
func TestComposeWithEitherPrism(t *testing.T) {
// Create an isomorphism between string and []byte
stringBytesIso := I.MakeIso(
func(s string) []byte { return []byte(s) },
func(b []byte) string { return string(b) },
)
// Create a prism that extracts Right values from Either[error, string]
rightPrism := P.FromEither[error, string]()
// Compose them
bytesPrism := Compose[E.Either[error, string]](stringBytesIso)(rightPrism)
t.Run("GetOption extracts and transforms Right value", func(t *testing.T) {
success := E.Right[error]("hello")
result := bytesPrism.GetOption(success)
assert.True(t, O.IsSome(result))
bytes := O.GetOrElse(F.Constant([]byte{}))(result)
assert.Equal(t, []byte("hello"), bytes)
})
t.Run("GetOption returns None for Left value", func(t *testing.T) {
failure := E.Left[string](errors.New("error"))
result := bytesPrism.GetOption(failure)
assert.True(t, O.IsNone(result))
})
t.Run("ReverseGet constructs Either from transformed value", func(t *testing.T) {
bytes := []byte("world")
result := bytesPrism.ReverseGet(bytes)
assert.True(t, E.IsRight(result))
str := E.GetOrElse(func(error) string { return "" })(result)
assert.Equal(t, "world", str)
})
t.Run("Round-trip through GetOption and ReverseGet", func(t *testing.T) {
// Start with bytes
original := []byte("test")
// ReverseGet to create Either
either := bytesPrism.ReverseGet(original)
// GetOption to extract bytes back
result := bytesPrism.GetOption(either)
assert.True(t, O.IsSome(result))
extracted := O.GetOrElse(F.Constant([]byte{}))(result)
assert.Equal(t, original, extracted)
})
}
// TestComposeWithOptionPrism tests composing an isomorphism with an Option prism
func TestComposeWithOptionPrism(t *testing.T) {
// Create an isomorphism between int and string
intStringIso := I.MakeIso(
func(i int) string { return strconv.Itoa(i) },
func(s string) int {
i, _ := strconv.Atoi(s)
return i
},
)
// Create a prism that extracts Some values from Option[int]
somePrism := P.FromOption[int]()
// Compose them
stringPrism := Compose[O.Option[int]](intStringIso)(somePrism)
t.Run("GetOption extracts and transforms Some value", func(t *testing.T) {
some := O.Some(42)
result := stringPrism.GetOption(some)
assert.True(t, O.IsSome(result))
str := O.GetOrElse(F.Constant(""))(result)
assert.Equal(t, "42", str)
})
t.Run("GetOption returns None for None value", func(t *testing.T) {
none := O.None[int]()
result := stringPrism.GetOption(none)
assert.True(t, O.IsNone(result))
})
t.Run("ReverseGet constructs Option from transformed value", func(t *testing.T) {
str := "100"
result := stringPrism.ReverseGet(str)
assert.True(t, O.IsSome(result))
num := O.GetOrElse(F.Constant(0))(result)
assert.Equal(t, 100, num)
})
}
// TestComposeWithCustomPrism tests composing with a custom prism
// Custom types for TestComposeWithCustomPrism
type Celsius float64
type Fahrenheit float64
type Temperature interface {
isTemperature()
}
type CelsiusTemp struct {
Value Celsius
}
func (c CelsiusTemp) isTemperature() {}
type KelvinTemp struct {
Value float64
}
func (k KelvinTemp) isTemperature() {}
func TestComposeWithCustomPrism(t *testing.T) {
// Isomorphism between Celsius and Fahrenheit
tempIso := I.MakeIso(
func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
)
// Prism that extracts Celsius from Temperature
celsiusPrism := P.MakePrism(
func(t Temperature) O.Option[Celsius] {
if ct, ok := t.(CelsiusTemp); ok {
return O.Some(ct.Value)
}
return O.None[Celsius]()
},
func(c Celsius) Temperature {
return CelsiusTemp{Value: c}
},
)
// Compose to work with Fahrenheit
fahrenheitPrism := Compose[Temperature](tempIso)(celsiusPrism)
t.Run("GetOption extracts and converts Celsius to Fahrenheit", func(t *testing.T) {
temp := CelsiusTemp{Value: 0}
result := fahrenheitPrism.GetOption(temp)
assert.True(t, O.IsSome(result))
fahrenheit := O.GetOrElse(F.Constant(Fahrenheit(0)))(result)
assert.InDelta(t, 32.0, float64(fahrenheit), 0.01)
})
t.Run("GetOption returns None for non-Celsius temperature", func(t *testing.T) {
temp := KelvinTemp{Value: 273.15}
result := fahrenheitPrism.GetOption(temp)
assert.True(t, O.IsNone(result))
})
t.Run("ReverseGet constructs Temperature from Fahrenheit", func(t *testing.T) {
fahrenheit := Fahrenheit(68)
result := fahrenheitPrism.ReverseGet(fahrenheit)
celsiusTemp, ok := result.(CelsiusTemp)
assert.True(t, ok)
assert.InDelta(t, 20.0, float64(celsiusTemp.Value), 0.01)
})
t.Run("Round-trip preserves value", func(t *testing.T) {
original := Fahrenheit(100)
// ReverseGet to create Temperature
temp := fahrenheitPrism.ReverseGet(original)
// GetOption to extract Fahrenheit back
result := fahrenheitPrism.GetOption(temp)
assert.True(t, O.IsSome(result))
extracted := O.GetOrElse(F.Constant(Fahrenheit(0)))(result)
assert.InDelta(t, float64(original), float64(extracted), 0.01)
})
}
// TestComposeIdentityIso tests composing with an identity isomorphism
func TestComposeIdentityIso(t *testing.T) {
// Identity isomorphism (no transformation)
idIso := I.Id[string]()
// Prism that extracts Right values
rightPrism := P.FromEither[error, string]()
// Compose with identity should not change behavior
composedPrism := Compose[E.Either[error, string]](idIso)(rightPrism)
t.Run("Composed prism behaves like original prism", func(t *testing.T) {
success := E.Right[error]("test")
// Original prism
originalResult := rightPrism.GetOption(success)
// Composed prism
composedResult := composedPrism.GetOption(success)
assert.Equal(t, originalResult, composedResult)
})
t.Run("ReverseGet produces same result", func(t *testing.T) {
value := "test"
// Original prism
originalEither := rightPrism.ReverseGet(value)
// Composed prism
composedEither := composedPrism.ReverseGet(value)
assert.Equal(t, originalEither, composedEither)
})
}
// TestComposeChaining tests chaining multiple compositions
func TestComposeChaining(t *testing.T) {
// First isomorphism: int to string
intStringIso := I.MakeIso(
func(i int) string { return strconv.Itoa(i) },
func(s string) int {
i, _ := strconv.Atoi(s)
return i
},
)
// Second isomorphism: string to []byte
stringBytesIso := I.MakeIso(
func(s string) []byte { return []byte(s) },
func(b []byte) string { return string(b) },
)
// Prism that extracts Right values
rightPrism := P.FromEither[error, int]()
// Chain compositions: Either[error, int] -> int -> string -> []byte
step1 := Compose[E.Either[error, int]](intStringIso)(rightPrism) // Prism[Either[error, int], string]
step2 := Compose[E.Either[error, int]](stringBytesIso)(step1) // Prism[Either[error, int], []byte]
t.Run("Chained composition extracts and transforms correctly", func(t *testing.T) {
either := E.Right[error](42)
result := step2.GetOption(either)
assert.True(t, O.IsSome(result))
bytes := O.GetOrElse(F.Constant([]byte{}))(result)
assert.Equal(t, []byte("42"), bytes)
})
t.Run("Chained composition ReverseGet works correctly", func(t *testing.T) {
bytes := []byte("100")
result := step2.ReverseGet(bytes)
assert.True(t, E.IsRight(result))
num := E.GetOrElse(func(error) int { return 0 })(result)
assert.Equal(t, 100, num)
})
}
// TestComposePrismLaws verifies that the composed prism satisfies prism laws
func TestComposePrismLaws(t *testing.T) {
// Create an isomorphism
iso := I.MakeIso(
func(i int) string { return strconv.Itoa(i) },
func(s string) int {
i, _ := strconv.Atoi(s)
return i
},
)
// Create a prism
prism := P.FromEither[error, int]()
// Compose them
composed := Compose[E.Either[error, int]](iso)(prism)
t.Run("Law 1: GetOption(ReverseGet(b)) == Some(b)", func(t *testing.T) {
value := "42"
// ReverseGet then GetOption should return Some(value)
either := composed.ReverseGet(value)
result := composed.GetOption(either)
assert.True(t, O.IsSome(result))
extracted := O.GetOrElse(F.Constant(""))(result)
assert.Equal(t, value, extracted)
})
t.Run("Law 2: If GetOption(s) == Some(a), then GetOption(ReverseGet(a)) == Some(a)", func(t *testing.T) {
either := E.Right[error](100)
// First GetOption
firstResult := composed.GetOption(either)
assert.True(t, O.IsSome(firstResult))
// Extract the value
value := O.GetOrElse(F.Constant(""))(firstResult)
// ReverseGet then GetOption again
reconstructed := composed.ReverseGet(value)
secondResult := composed.GetOption(reconstructed)
assert.True(t, O.IsSome(secondResult))
finalValue := O.GetOrElse(F.Constant(""))(secondResult)
assert.Equal(t, value, finalValue)
})
}
// TestComposeWithEmptyValues tests edge cases with empty/zero values
func TestComposeWithEmptyValues(t *testing.T) {
// Isomorphism that handles empty strings
iso := I.MakeIso(
func(s string) []byte { return []byte(s) },
func(b []byte) string { return string(b) },
)
prism := P.FromEither[error, string]()
composed := Compose[E.Either[error, string]](iso)(prism)
t.Run("Empty string is handled correctly", func(t *testing.T) {
either := E.Right[error]("")
result := composed.GetOption(either)
assert.True(t, O.IsSome(result))
bytes := O.GetOrElse(F.Constant([]byte("default")))(result)
assert.Equal(t, []byte{}, bytes)
})
t.Run("ReverseGet with empty bytes", func(t *testing.T) {
bytes := []byte{}
result := composed.ReverseGet(bytes)
assert.True(t, E.IsRight(result))
str := E.GetOrElse(func(error) string { return "default" })(result)
assert.Equal(t, "", str)
})
}

View File

@@ -0,0 +1,112 @@
// 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 iso provides utilities for composing isomorphisms with prisms.
//
// This package enables the composition of isomorphisms (bidirectional transformations)
// with prisms (optics for sum types), allowing you to transform the focus type of a prism
// using an isomorphism. This is particularly useful when you need to work with prisms
// that focus on a type that can be bidirectionally converted to another type.
//
// # Key Concepts
//
// An Iso[S, A] represents a bidirectional transformation between types S and A without
// loss of information. A Prism[S, A] is an optic that focuses on a specific variant
// within a sum type S, extracting values of type A.
//
// When you compose an Iso[A, B] with a Prism[S, A], you get a Prism[S, B] that:
// - Extracts values of type A from S (using the prism)
// - Transforms them to type B (using the isomorphism's Get)
// - Can construct S from B by reversing the transformation (using the isomorphism's ReverseGet)
//
// # Example Usage
//
// import (
// "github.com/IBM/fp-go/v2/optics/iso"
// "github.com/IBM/fp-go/v2/optics/prism"
// PI "github.com/IBM/fp-go/v2/optics/prism/iso"
// O "github.com/IBM/fp-go/v2/option"
// )
//
// // Create an isomorphism between string and []byte
// stringBytesIso := iso.MakeIso(
// func(s string) []byte { return []byte(s) },
// func(b []byte) string { return string(b) },
// )
//
// // Create a prism that extracts Right values from Either[error, string]
// rightPrism := prism.FromEither[error, string]()
//
// // Compose them to get a prism that extracts Right values as []byte
// bytesPrism := PI.Compose(stringBytesIso)(rightPrism)
//
// // Use the composed prism
// either := either.Right[error]("hello")
// result := bytesPrism.GetOption(either) // Some([]byte("hello"))
//
// # Type Aliases
//
// This package re-exports key types from the iso and prism packages for convenience:
// - Iso[S, A]: An isomorphism between types S and A
// - Prism[S, A]: A prism focusing on type A within sum type S
// - Operator[S, A, B]: A function that transforms Prism[S, A] to Prism[S, B]
package iso
import (
I "github.com/IBM/fp-go/v2/optics/iso"
P "github.com/IBM/fp-go/v2/optics/prism"
)
type (
// Iso represents an isomorphism between types S and A.
// It is a bidirectional transformation that converts between two types
// without any loss of information.
//
// Type Parameters:
// - S: The source type
// - A: The target type
//
// See github.com/IBM/fp-go/v2/optics/iso for the full Iso API.
Iso[S, A any] = I.Iso[S, A]
// Prism is an optic used to select part of a sum type (tagged union).
// It provides operations to extract and construct values within sum types.
//
// Type Parameters:
// - S: The source type (sum type)
// - A: The focus type (variant within the sum type)
//
// See github.com/IBM/fp-go/v2/optics/prism for the full Prism API.
Prism[S, A any] = P.Prism[S, A]
// Operator represents a function that transforms one prism into another.
// It takes a Prism[S, A] and returns a Prism[S, B], allowing for prism transformations.
//
// This is commonly used with the Compose function to create operators that
// transform the focus type of a prism using an isomorphism.
//
// Type Parameters:
// - S: The source type (remains constant)
// - A: The original focus type
// - B: The new focus type
//
// Example:
//
// // Create an operator that transforms string prisms to []byte prisms
// stringToBytesOp := Compose(stringBytesIso)
// // Apply it to a prism
// bytesPrism := stringToBytesOp(stringPrism)
Operator[S, A, B any] = P.Operator[S, A, B]
)

View File

@@ -23,8 +23,10 @@ import (
"strconv"
"time"
"github.com/IBM/fp-go/v2/array"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
J "github.com/IBM/fp-go/v2/json"
"github.com/IBM/fp-go/v2/option"
S "github.com/IBM/fp-go/v2/string"
)
@@ -322,6 +324,50 @@ func FromEither[E, T any]() Prism[Either[E, T], T] {
return MakePrismWithName(either.ToOption[E, T], either.Of[E, T], "PrismFromEither")
}
// FromResult creates a prism for extracting values from Result types.
// It provides a safe way to work with Result values (which are Either[error, T]),
// focusing on the success case and handling errors gracefully through the Option type.
//
// This is a convenience function that is equivalent to FromEither[error, T]().
//
// The prism's GetOption attempts to extract the success value from a Result.
// If the Result is successful, it returns Some(value); if it's an error, it returns None.
//
// The prism's ReverseGet always succeeds, wrapping a value into a successful Result.
//
// Type Parameters:
// - T: The value type contained in the Result
//
// Returns:
// - A Prism[Result[T], T] that safely extracts success values
//
// Example:
//
// // Create a prism for extracting successful results
// resultPrism := FromResult[int]()
//
// // Extract from successful result
// success := result.Of[int](42)
// value := resultPrism.GetOption(success) // Some(42)
//
// // Extract from error result
// failure := result.Error[int](errors.New("failed"))
// value = resultPrism.GetOption(failure) // None[int]()
//
// // Wrap value into successful Result
// wrapped := resultPrism.ReverseGet(100) // Result containing 100
//
// // Use with Set to update successful results
// setter := Set[Result[int], int](200)
// result := setter(resultPrism)(success) // Result containing 200
// result = setter(resultPrism)(failure) // Error result (unchanged)
//
// Common use cases:
// - Extracting successful values from Result types
// - Filtering out errors in data pipelines
// - Working with fallible operations that return Result
// - Composing with other prisms for complex error handling
//
//go:inline
func FromResult[T any]() Prism[Result[T], T] {
return FromEither[error, T]()
@@ -1261,3 +1307,71 @@ func MakeURLPrisms() URLPrisms {
RawFragment: _prismRawFragment,
}
}
// ParseJSON creates a prism for parsing and marshaling JSON data.
// It provides a safe way to convert between JSON bytes and Go types,
// handling parsing and marshaling errors gracefully through the Option type.
//
// The prism's GetOption attempts to unmarshal JSON bytes into type A.
// If unmarshaling succeeds, it returns Some(A); if it fails (e.g., invalid JSON
// or type mismatch), it returns None.
//
// The prism's ReverseGet marshals a value of type A into JSON bytes.
// If marshaling fails (which is rare), it returns an empty byte slice.
//
// Type Parameters:
// - A: The Go type to unmarshal JSON into
//
// Returns:
// - A Prism[[]byte, A] that safely handles JSON parsing/marshaling
//
// Example:
//
// // Define a struct type
// type Person struct {
// Name string `json:"name"`
// Age int `json:"age"`
// }
//
// // Create a JSON parsing prism
// jsonPrism := ParseJSON[Person]()
//
// // Parse valid JSON
// jsonData := []byte(`{"name":"Alice","age":30}`)
// person := jsonPrism.GetOption(jsonData)
// // Some(Person{Name: "Alice", Age: 30})
//
// // Parse invalid JSON
// invalidJSON := []byte(`{invalid json}`)
// result := jsonPrism.GetOption(invalidJSON) // None[Person]()
//
// // Marshal to JSON
// p := Person{Name: "Bob", Age: 25}
// jsonBytes := jsonPrism.ReverseGet(p)
// // []byte(`{"name":"Bob","age":25}`)
//
// // Use with Set to update JSON data
// newPerson := Person{Name: "Charlie", Age: 35}
// setter := Set[[]byte, Person](newPerson)
// updated := setter(jsonPrism)(jsonData)
// // []byte(`{"name":"Charlie","age":35}`)
//
// Common use cases:
// - Parsing JSON configuration files
// - Working with JSON API responses
// - Validating and transforming JSON data in pipelines
// - Type-safe JSON deserialization
// - Converting between JSON and Go structs
func ParseJSON[A any]() Prism[[]byte, A] {
return MakePrismWithName(
F.Flow2(
J.Unmarshal[A],
either.ToOption[error, A],
),
F.Flow2(
J.Marshal[A],
either.GetOrElse(F.Constant1[error](array.Empty[byte]())),
),
"JSON",
)
}

View File

@@ -16,11 +16,14 @@
package prism
import (
"errors"
"regexp"
"testing"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
@@ -1396,3 +1399,403 @@ func TestNonEmptyStringValidation(t *testing.T) {
assert.Equal(t, []string{"hello", "world", "test"}, nonEmpty)
})
}
// TestFromResult tests the FromResult prism with Result types
func TestFromResult(t *testing.T) {
t.Run("extract from successful result", func(t *testing.T) {
prism := FromResult[int]()
success := result.Of[int](42)
extracted := prism.GetOption(success)
assert.True(t, O.IsSome(extracted))
assert.Equal(t, 42, O.GetOrElse(F.Constant(-1))(extracted))
})
t.Run("extract from error result", func(t *testing.T) {
prism := FromResult[int]()
failure := E.Left[int](errors.New("test error"))
extracted := prism.GetOption(failure)
assert.True(t, O.IsNone(extracted))
})
t.Run("ReverseGet wraps value in successful result", func(t *testing.T) {
prism := FromResult[int]()
wrapped := prism.ReverseGet(100)
// Verify it's a successful result
extracted := prism.GetOption(wrapped)
assert.True(t, O.IsSome(extracted))
assert.Equal(t, 100, O.GetOrElse(F.Constant(-1))(extracted))
})
t.Run("works with string type", func(t *testing.T) {
prism := FromResult[string]()
success := result.Of[string]("hello")
extracted := prism.GetOption(success)
assert.True(t, O.IsSome(extracted))
assert.Equal(t, "hello", O.GetOrElse(F.Constant(""))(extracted))
})
t.Run("works with struct type", func(t *testing.T) {
type Person struct {
Name string
Age int
}
prism := FromResult[Person]()
person := Person{Name: "Alice", Age: 30}
success := result.Of[Person](person)
extracted := prism.GetOption(success)
assert.True(t, O.IsSome(extracted))
result := O.GetOrElse(F.Constant(Person{}))(extracted)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
}
// TestFromResultWithSet tests using Set with FromResult prism
func TestFromResultWithSet(t *testing.T) {
t.Run("set on successful result", func(t *testing.T) {
prism := FromResult[int]()
setter := Set[result.Result[int], int](200)
success := result.Of[int](42)
updated := setter(prism)(success)
// Verify the value was updated
extracted := prism.GetOption(updated)
assert.True(t, O.IsSome(extracted))
assert.Equal(t, 200, O.GetOrElse(F.Constant(-1))(extracted))
})
t.Run("set on error result leaves it unchanged", func(t *testing.T) {
prism := FromResult[int]()
setter := Set[result.Result[int], int](200)
failure := E.Left[int](errors.New("test error"))
updated := setter(prism)(failure)
// Verify it's still an error
extracted := prism.GetOption(updated)
assert.True(t, O.IsNone(extracted))
})
}
// TestFromResultPrismLaws tests that FromResult satisfies prism laws
func TestFromResultPrismLaws(t *testing.T) {
prism := FromResult[int]()
t.Run("law 1: GetOption(ReverseGet(a)) == Some(a)", func(t *testing.T) {
value := 42
wrapped := prism.ReverseGet(value)
extracted := prism.GetOption(wrapped)
assert.True(t, O.IsSome(extracted))
assert.Equal(t, value, O.GetOrElse(F.Constant(-1))(extracted))
})
t.Run("law 2: ReverseGet is consistent", func(t *testing.T) {
value := 42
result1 := prism.ReverseGet(value)
result2 := prism.ReverseGet(value)
// Both should extract the same value
extracted1 := prism.GetOption(result1)
extracted2 := prism.GetOption(result2)
val1 := O.GetOrElse(F.Constant(-1))(extracted1)
val2 := O.GetOrElse(F.Constant(-1))(extracted2)
assert.Equal(t, val1, val2)
})
}
// TestFromResultComposition tests composing FromResult with other prisms
func TestFromResultComposition(t *testing.T) {
t.Run("compose with predicate prism", func(t *testing.T) {
// Create a prism that only matches positive numbers
positivePrism := FromPredicate(func(n int) bool { return n > 0 })
// Compose: Result[int] -> int -> positive int
composed := Compose[result.Result[int]](positivePrism)(FromResult[int]())
// Test with positive number
success := result.Of[int](42)
extracted := composed.GetOption(success)
assert.True(t, O.IsSome(extracted))
assert.Equal(t, 42, O.GetOrElse(F.Constant(-1))(extracted))
// Test with negative number
negativeSuccess := result.Of[int](-5)
extracted = composed.GetOption(negativeSuccess)
assert.True(t, O.IsNone(extracted))
// Test with error
failure := E.Left[int](errors.New("test error"))
extracted = composed.GetOption(failure)
assert.True(t, O.IsNone(extracted))
})
}
// TestParseJSON tests the ParseJSON prism with various JSON data
func TestParseJSON(t *testing.T) {
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
t.Run("parse valid JSON", func(t *testing.T) {
prism := ParseJSON[Person]()
jsonData := []byte(`{"name":"Alice","age":30}`)
parsed := prism.GetOption(jsonData)
assert.True(t, O.IsSome(parsed))
person := O.GetOrElse(F.Constant(Person{}))(parsed)
assert.Equal(t, "Alice", person.Name)
assert.Equal(t, 30, person.Age)
})
t.Run("parse invalid JSON", func(t *testing.T) {
prism := ParseJSON[Person]()
invalidJSON := []byte(`{invalid json}`)
parsed := prism.GetOption(invalidJSON)
assert.True(t, O.IsNone(parsed))
})
t.Run("parse JSON with missing fields", func(t *testing.T) {
prism := ParseJSON[Person]()
// Missing age field - should use zero value
jsonData := []byte(`{"name":"Bob"}`)
parsed := prism.GetOption(jsonData)
assert.True(t, O.IsSome(parsed))
person := O.GetOrElse(F.Constant(Person{}))(parsed)
assert.Equal(t, "Bob", person.Name)
assert.Equal(t, 0, person.Age)
})
t.Run("parse JSON with extra fields", func(t *testing.T) {
prism := ParseJSON[Person]()
// Extra field should be ignored
jsonData := []byte(`{"name":"Charlie","age":25,"extra":"ignored"}`)
parsed := prism.GetOption(jsonData)
assert.True(t, O.IsSome(parsed))
person := O.GetOrElse(F.Constant(Person{}))(parsed)
assert.Equal(t, "Charlie", person.Name)
assert.Equal(t, 25, person.Age)
})
t.Run("ReverseGet marshals to JSON", func(t *testing.T) {
prism := ParseJSON[Person]()
person := Person{Name: "David", Age: 35}
jsonBytes := prism.ReverseGet(person)
// Parse it back to verify
parsed := prism.GetOption(jsonBytes)
assert.True(t, O.IsSome(parsed))
result := O.GetOrElse(F.Constant(Person{}))(parsed)
assert.Equal(t, "David", result.Name)
assert.Equal(t, 35, result.Age)
})
t.Run("works with primitive types", func(t *testing.T) {
prism := ParseJSON[int]()
jsonData := []byte(`42`)
parsed := prism.GetOption(jsonData)
assert.True(t, O.IsSome(parsed))
assert.Equal(t, 42, O.GetOrElse(F.Constant(-1))(parsed))
})
t.Run("works with arrays", func(t *testing.T) {
prism := ParseJSON[[]string]()
jsonData := []byte(`["hello","world","test"]`)
parsed := prism.GetOption(jsonData)
assert.True(t, O.IsSome(parsed))
arr := O.GetOrElse(F.Constant([]string{}))(parsed)
assert.Equal(t, []string{"hello", "world", "test"}, arr)
})
t.Run("works with maps", func(t *testing.T) {
prism := ParseJSON[map[string]int]()
jsonData := []byte(`{"a":1,"b":2,"c":3}`)
parsed := prism.GetOption(jsonData)
assert.True(t, O.IsSome(parsed))
m := O.GetOrElse(F.Constant(map[string]int{}))(parsed)
assert.Equal(t, 1, m["a"])
assert.Equal(t, 2, m["b"])
assert.Equal(t, 3, m["c"])
})
t.Run("works with nested structures", func(t *testing.T) {
type Address struct {
Street string `json:"street"`
City string `json:"city"`
}
type PersonWithAddress struct {
Name string `json:"name"`
Address Address `json:"address"`
}
prism := ParseJSON[PersonWithAddress]()
jsonData := []byte(`{"name":"Eve","address":{"street":"123 Main St","city":"NYC"}}`)
parsed := prism.GetOption(jsonData)
assert.True(t, O.IsSome(parsed))
person := O.GetOrElse(F.Constant(PersonWithAddress{}))(parsed)
assert.Equal(t, "Eve", person.Name)
assert.Equal(t, "123 Main St", person.Address.Street)
assert.Equal(t, "NYC", person.Address.City)
})
t.Run("parse empty JSON object", func(t *testing.T) {
prism := ParseJSON[Person]()
jsonData := []byte(`{}`)
parsed := prism.GetOption(jsonData)
assert.True(t, O.IsSome(parsed))
person := O.GetOrElse(F.Constant(Person{}))(parsed)
assert.Equal(t, "", person.Name)
assert.Equal(t, 0, person.Age)
})
t.Run("parse null JSON", func(t *testing.T) {
prism := ParseJSON[*Person]()
jsonData := []byte(`null`)
parsed := prism.GetOption(jsonData)
assert.True(t, O.IsSome(parsed))
person := O.GetOrElse(F.Constant(&Person{}))(parsed)
assert.Nil(t, person)
})
}
// TestParseJSONWithSet tests using Set with ParseJSON prism
func TestParseJSONWithSet(t *testing.T) {
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
t.Run("set updates JSON data", func(t *testing.T) {
prism := ParseJSON[Person]()
originalJSON := []byte(`{"name":"Alice","age":30}`)
newPerson := Person{Name: "Bob", Age: 25}
setter := Set[[]byte, Person](newPerson)
updatedJSON := setter(prism)(originalJSON)
// Parse the updated JSON
parsed := prism.GetOption(updatedJSON)
assert.True(t, O.IsSome(parsed))
person := O.GetOrElse(F.Constant(Person{}))(parsed)
assert.Equal(t, "Bob", person.Name)
assert.Equal(t, 25, person.Age)
})
t.Run("set on invalid JSON returns original unchanged", func(t *testing.T) {
prism := ParseJSON[Person]()
invalidJSON := []byte(`{invalid}`)
newPerson := Person{Name: "Charlie", Age: 35}
setter := Set[[]byte, Person](newPerson)
result := setter(prism)(invalidJSON)
// Should return original unchanged since it couldn't be parsed
assert.Equal(t, invalidJSON, result)
})
}
// TestParseJSONPrismLaws tests that ParseJSON satisfies prism laws
func TestParseJSONPrismLaws(t *testing.T) {
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
prism := ParseJSON[Person]()
t.Run("law 1: GetOption(ReverseGet(a)) == Some(a)", func(t *testing.T) {
person := Person{Name: "Alice", Age: 30}
jsonBytes := prism.ReverseGet(person)
parsed := prism.GetOption(jsonBytes)
assert.True(t, O.IsSome(parsed))
result := O.GetOrElse(F.Constant(Person{}))(parsed)
assert.Equal(t, person.Name, result.Name)
assert.Equal(t, person.Age, result.Age)
})
t.Run("law 2: ReverseGet is consistent", func(t *testing.T) {
person := Person{Name: "Bob", Age: 25}
json1 := prism.ReverseGet(person)
json2 := prism.ReverseGet(person)
// Both should parse to the same value
parsed1 := prism.GetOption(json1)
parsed2 := prism.GetOption(json2)
result1 := O.GetOrElse(F.Constant(Person{}))(parsed1)
result2 := O.GetOrElse(F.Constant(Person{}))(parsed2)
assert.Equal(t, result1.Name, result2.Name)
assert.Equal(t, result1.Age, result2.Age)
})
}
// TestParseJSONComposition tests composing ParseJSON with other prisms
func TestParseJSONComposition(t *testing.T) {
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
t.Run("compose with predicate prism", func(t *testing.T) {
// Create a prism that only matches adults (age >= 18)
adultPrism := FromPredicate(func(p Person) bool { return p.Age >= 18 })
// Compose: []byte -> Person -> Adult
composed := Compose[[]byte](adultPrism)(ParseJSON[Person]())
// Test with adult
adultJSON := []byte(`{"name":"Alice","age":30}`)
parsed := composed.GetOption(adultJSON)
assert.True(t, O.IsSome(parsed))
person := O.GetOrElse(F.Constant(Person{}))(parsed)
assert.Equal(t, "Alice", person.Name)
// Test with minor
minorJSON := []byte(`{"name":"Bob","age":15}`)
parsed = composed.GetOption(minorJSON)
assert.True(t, O.IsNone(parsed))
// Test with invalid JSON
invalidJSON := []byte(`{invalid}`)
parsed = composed.GetOption(invalidJSON)
assert.True(t, O.IsNone(parsed))
})
}

View File

@@ -3,7 +3,7 @@ package readerio
import "github.com/IBM/fp-go/v2/io"
//go:inline
func ChainConsumer[R, A any](c Consumer[A]) Operator[R, A, struct{}] {
func ChainConsumer[R, A any](c Consumer[A]) Operator[R, A, Void] {
return ChainIOK[R](io.FromConsumer(c))
}

View File

@@ -18,6 +18,7 @@ package readerio
import (
"github.com/IBM/fp-go/v2/consumer"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/reader"
@@ -66,4 +67,6 @@ type (
// Predicate represents a function that tests a value of type A and returns a boolean.
// It's commonly used for filtering and conditional operations.
Predicate[A any] = predicate.Predicate[A]
Void = function.Void
)

View File

@@ -1,14 +1,107 @@
// 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 readerioresult
import (
"github.com/IBM/fp-go/v2/readerioeither"
)
// ChainConsumer chains a consumer (side-effect function) into a ReaderIOResult computation,
// replacing the success value with Void (empty struct).
//
// This is useful for performing side effects (like logging, printing, or writing to a file)
// where you don't need to preserve the original value. The consumer is only executed if the
// computation succeeds; if it fails with an error, the consumer is skipped.
//
// Type parameters:
// - R: The context/environment type
// - A: The value type to consume
//
// Parameters:
// - c: A consumer function that performs a side effect on the value
//
// Returns:
//
// An Operator that executes the consumer and returns Void on success
//
// Example:
//
// import (
// "context"
// "fmt"
// RIO "github.com/IBM/fp-go/v2/readerioresult"
// )
//
// // Log a value and discard it
// logValue := RIO.ChainConsumer[context.Context](func(x int) {
// fmt.Printf("Value: %d\n", x)
// })
//
// computation := F.Pipe1(
// RIO.Of[context.Context](42),
// logValue,
// )
// // Prints "Value: 42" and returns result.Of(struct{}{})
//
//go:inline
func ChainConsumer[R, A any](c Consumer[A]) Operator[R, A, struct{}] {
func ChainConsumer[R, A any](c Consumer[A]) Operator[R, A, Void] {
return readerioeither.ChainConsumer[R, error](c)
}
// ChainFirstConsumer chains a consumer into a ReaderIOResult computation while preserving
// the original value.
//
// This is useful for performing side effects (like logging, printing, or metrics collection)
// where you want to keep the original value for further processing. The consumer is only
// executed if the computation succeeds; if it fails with an error, the consumer is skipped
// and the error is propagated.
//
// Type parameters:
// - R: The context/environment type
// - A: The value type to consume and preserve
//
// Parameters:
// - c: A consumer function that performs a side effect on the value
//
// Returns:
//
// An Operator that executes the consumer and returns the original value on success
//
// Example:
//
// import (
// "context"
// "fmt"
// F "github.com/IBM/fp-go/v2/function"
// N "github.com/IBM/fp-go/v2/number"
// RIO "github.com/IBM/fp-go/v2/readerioresult"
// )
//
// // Log a value but keep it for further processing
// logValue := RIO.ChainFirstConsumer[context.Context](func(x int) {
// fmt.Printf("Processing: %d\n", x)
// })
//
// computation := F.Pipe2(
// RIO.Of[context.Context](10),
// logValue,
// RIO.Map[context.Context](N.Mul(2)),
// )
// // Prints "Processing: 10" and returns result.Of(20)
//
//go:inline
func ChainFirstConsumer[R, A any](c Consumer[A]) Operator[R, A, A] {
return readerioeither.ChainFirstConsumer[R, error](c)

View File

@@ -0,0 +1,360 @@
// 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 readerioresult
import (
"context"
"errors"
"testing"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/result"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
// TestChainConsumer_Success tests that ChainConsumer executes the consumer
// and returns Void when the computation succeeds
func TestChainConsumer_Success(t *testing.T) {
// Track if consumer was called
var consumed int
consumer := func(x int) {
consumed = x
}
// Create a successful computation and chain the consumer
computation := F.Pipe1(
Of[context.Context](42),
ChainConsumer[context.Context](consumer),
)
// Execute the computation
res := computation(context.Background())()
// Verify consumer was called with correct value
assert.Equal(t, 42, consumed)
// Verify result is successful with Void
assert.True(t, result.IsRight(res))
if result.IsRight(res) {
val := result.GetOrElse(func(error) Void { return Void{} })(res)
assert.Equal(t, Void{}, val)
}
}
// TestChainConsumer_Failure tests that ChainConsumer does not execute
// the consumer when the computation fails
func TestChainConsumer_Failure(t *testing.T) {
// Track if consumer was called
consumerCalled := false
consumer := func(x int) {
consumerCalled = true
}
// Create a failing computation
expectedErr := errors.New("test error")
computation := F.Pipe1(
Left[context.Context, int](expectedErr),
ChainConsumer[context.Context](consumer),
)
// Execute the computation
res := computation(context.Background())()
// Verify consumer was NOT called
assert.False(t, consumerCalled)
// Verify result is an error
assert.True(t, result.IsLeft(res))
}
// TestChainConsumer_MultipleOperations tests chaining multiple operations
// with ChainConsumer in a pipeline
func TestChainConsumer_MultipleOperations(t *testing.T) {
// Track consumer calls
var values []int
consumer := func(x int) {
values = append(values, x)
}
// Create a pipeline with multiple operations
computation := F.Pipe2(
Of[context.Context](10),
Map[context.Context](N.Mul(2)),
ChainConsumer[context.Context](consumer),
)
// Execute the computation
res := computation(context.Background())()
// Verify consumer was called with transformed value
assert.Equal(t, []int{20}, values)
// Verify result is successful
assert.True(t, result.IsRight(res))
}
// TestChainFirstConsumer_Success tests that ChainFirstConsumer executes
// the consumer and preserves the original value
func TestChainFirstConsumer_Success(t *testing.T) {
// Track if consumer was called
var consumed int
consumer := func(x int) {
consumed = x
}
// Create a successful computation and chain the consumer
computation := F.Pipe1(
Of[context.Context](42),
ChainFirstConsumer[context.Context](consumer),
)
// Execute the computation
res := computation(context.Background())()
// Verify consumer was called with correct value
assert.Equal(t, 42, consumed)
// Verify result is successful and preserves original value
assert.True(t, result.IsRight(res))
if result.IsRight(res) {
val := result.GetOrElse(func(error) int { return 0 })(res)
assert.Equal(t, 42, val)
}
}
// TestChainFirstConsumer_Failure tests that ChainFirstConsumer does not
// execute the consumer when the computation fails
func TestChainFirstConsumer_Failure(t *testing.T) {
// Track if consumer was called
consumerCalled := false
consumer := func(x int) {
consumerCalled = true
}
// Create a failing computation
expectedErr := errors.New("test error")
computation := F.Pipe1(
Left[context.Context, int](expectedErr),
ChainFirstConsumer[context.Context](consumer),
)
// Execute the computation
res := computation(context.Background())()
// Verify consumer was NOT called
assert.False(t, consumerCalled)
// Verify result is an error
assert.True(t, result.IsLeft(res))
}
// TestChainFirstConsumer_PreservesValue tests that ChainFirstConsumer
// preserves the value for further processing
func TestChainFirstConsumer_PreservesValue(t *testing.T) {
// Track consumer calls
var logged []int
logger := func(x int) {
logged = append(logged, x)
}
// Create a pipeline that logs intermediate values
computation := F.Pipe3(
Of[context.Context](10),
ChainFirstConsumer[context.Context](logger),
Map[context.Context](N.Mul(2)),
ChainFirstConsumer[context.Context](logger),
)
// Execute the computation
res := computation(context.Background())()
// Verify consumer was called at each step
assert.Equal(t, []int{10, 20}, logged)
// Verify final result
assert.True(t, result.IsRight(res))
if result.IsRight(res) {
val := result.GetOrElse(func(error) int { return 0 })(res)
assert.Equal(t, 20, val)
}
}
// TestChainFirstConsumer_WithMap tests combining ChainFirstConsumer with Map
func TestChainFirstConsumer_WithMap(t *testing.T) {
// Track intermediate values
var intermediate int
consumer := func(x int) {
intermediate = x
}
// Create a pipeline with logging and transformation
computation := F.Pipe2(
Of[context.Context](5),
ChainFirstConsumer[context.Context](consumer),
Map[context.Context](N.Mul(3)),
)
// Execute the computation
res := computation(context.Background())()
// Verify consumer saw original value
assert.Equal(t, 5, intermediate)
// Verify final result is transformed
assert.True(t, result.IsRight(res))
if result.IsRight(res) {
val := result.GetOrElse(func(error) int { return 0 })(res)
assert.Equal(t, 15, val)
}
}
// TestChainConsumer_WithContext tests that consumers work with context
func TestChainConsumer_WithContext(t *testing.T) {
type Config struct {
Multiplier int
}
// Track consumer calls
var consumed int
consumer := func(x int) {
consumed = x
}
// Create a computation that uses context
computation := F.Pipe2(
Of[Config](10),
Map[Config](N.Mul(2)),
ChainConsumer[Config](consumer),
)
// Execute with context
cfg := Config{Multiplier: 3}
res := computation(cfg)()
// Verify consumer was called
assert.Equal(t, 20, consumed)
// Verify result is successful
assert.True(t, result.IsRight(res))
}
// TestChainFirstConsumer_SideEffects tests that ChainFirstConsumer
// can be used for side effects like logging
func TestChainFirstConsumer_SideEffects(t *testing.T) {
// Simulate a logging side effect
var logs []string
logValue := func(x string) {
logs = append(logs, "Processing: "+x)
}
// Create a pipeline with logging
computation := F.Pipe3(
Of[context.Context]("hello"),
ChainFirstConsumer[context.Context](logValue),
Map[context.Context](S.Append(" world")),
ChainFirstConsumer[context.Context](logValue),
)
// Execute the computation
res := computation(context.Background())()
// Verify logs were created
assert.Equal(t, []string{
"Processing: hello",
"Processing: hello world",
}, logs)
// Verify final result
assert.True(t, result.IsRight(res))
if result.IsRight(res) {
val := result.GetOrElse(func(error) string { return "" })(res)
assert.Equal(t, "hello world", val)
}
}
// TestChainConsumer_ComplexType tests consumers with complex types
func TestChainConsumer_ComplexType(t *testing.T) {
type User struct {
Name string
Age int
}
// Track consumed user
var consumedUser *User
consumer := func(u User) {
consumedUser = &u
}
// Create a computation with a complex type
user := User{Name: "Alice", Age: 30}
computation := F.Pipe1(
Of[context.Context](user),
ChainConsumer[context.Context](consumer),
)
// Execute the computation
res := computation(context.Background())()
// Verify consumer received the user
assert.NotNil(t, consumedUser)
assert.Equal(t, "Alice", consumedUser.Name)
assert.Equal(t, 30, consumedUser.Age)
// Verify result is successful
assert.True(t, result.IsRight(res))
}
// TestChainFirstConsumer_ComplexType tests ChainFirstConsumer with complex types
func TestChainFirstConsumer_ComplexType(t *testing.T) {
type Product struct {
ID int
Name string
Price float64
}
// Track consumed products
var consumedProducts []Product
consumer := func(p Product) {
consumedProducts = append(consumedProducts, p)
}
// Create a pipeline with complex type
product := Product{ID: 1, Name: "Widget", Price: 9.99}
computation := F.Pipe2(
Of[context.Context](product),
ChainFirstConsumer[context.Context](consumer),
Map[context.Context](func(p Product) Product {
p.Price = p.Price * 1.1 // Apply 10% markup
return p
}),
)
// Execute the computation
res := computation(context.Background())()
// Verify consumer saw original product
assert.Len(t, consumedProducts, 1)
assert.Equal(t, 9.99, consumedProducts[0].Price)
// Verify final result has updated price
assert.True(t, result.IsRight(res))
if result.IsRight(res) {
finalProduct := result.GetOrElse(func(error) Product { return Product{} })(res)
assert.InDelta(t, 10.989, finalProduct.Price, 0.001)
}
}

View File

@@ -25,10 +25,11 @@ import (
"github.com/IBM/fp-go/v2/readerio"
RIOE "github.com/IBM/fp-go/v2/readerioeither"
"github.com/IBM/fp-go/v2/readeroption"
"github.com/IBM/fp-go/v2/result"
)
//go:inline
func FromReaderOption[R, A any](onNone func() error) Kleisli[R, ReaderOption[R, A], A] {
func FromReaderOption[R, A any](onNone Lazy[error]) Kleisli[R, ReaderOption[R, A], A] {
return RIOE.FromReaderOption[R, A](onNone)
}
@@ -113,7 +114,7 @@ func MonadTap[R, A, B any](fa ReaderIOResult[R, A], f Kleisli[R, A, B]) ReaderIO
// The Either is automatically lifted into the ReaderIOResult context.
//
//go:inline
func MonadChainEitherK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B]) ReaderIOResult[R, B] {
func MonadChainEitherK[R, A, B any](ma ReaderIOResult[R, A], f result.Kleisli[A, B]) ReaderIOResult[R, B] {
return RIOE.MonadChainEitherK(ma, f)
}
@@ -121,7 +122,7 @@ func MonadChainEitherK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B]
// The Either is automatically lifted into the ReaderIOResult context.
//
//go:inline
func MonadChainResultK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B]) ReaderIOResult[R, B] {
func MonadChainResultK[R, A, B any](ma ReaderIOResult[R, A], f result.Kleisli[A, B]) ReaderIOResult[R, B] {
return RIOE.MonadChainEitherK(ma, f)
}
@@ -129,7 +130,7 @@ func MonadChainResultK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B]
// This is the curried version of MonadChainEitherK.
//
//go:inline
func ChainEitherK[R, A, B any](f func(A) Result[B]) Operator[R, A, B] {
func ChainEitherK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, B] {
return RIOE.ChainEitherK[R](f)
}
@@ -137,7 +138,7 @@ func ChainEitherK[R, A, B any](f func(A) Result[B]) Operator[R, A, B] {
// This is the curried version of MonadChainEitherK.
//
//go:inline
func ChainResultK[R, A, B any](f func(A) Result[B]) Operator[R, A, B] {
func ChainResultK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, B] {
return RIOE.ChainEitherK[R](f)
}
@@ -145,12 +146,12 @@ func ChainResultK[R, A, B any](f func(A) Result[B]) Operator[R, A, B] {
// Useful for validation or side effects that return Either.
//
//go:inline
func MonadChainFirstEitherK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B]) ReaderIOResult[R, A] {
func MonadChainFirstEitherK[R, A, B any](ma ReaderIOResult[R, A], f result.Kleisli[A, B]) ReaderIOResult[R, A] {
return RIOE.MonadChainFirstEitherK(ma, f)
}
//go:inline
func MonadTapEitherK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B]) ReaderIOResult[R, A] {
func MonadTapEitherK[R, A, B any](ma ReaderIOResult[R, A], f result.Kleisli[A, B]) ReaderIOResult[R, A] {
return RIOE.MonadTapEitherK(ma, f)
}
@@ -158,12 +159,12 @@ func MonadTapEitherK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B])
// This is the curried version of MonadChainFirstEitherK.
//
//go:inline
func ChainFirstEitherK[R, A, B any](f func(A) Result[B]) Operator[R, A, A] {
func ChainFirstEitherK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, A] {
return RIOE.ChainFirstEitherK[R](f)
}
//go:inline
func TapEitherK[R, A, B any](f func(A) Result[B]) Operator[R, A, A] {
func TapEitherK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, A] {
return RIOE.TapEitherK[R](f)
}
@@ -171,12 +172,12 @@ func TapEitherK[R, A, B any](f func(A) Result[B]) Operator[R, A, A] {
// Useful for validation or side effects that return Either.
//
//go:inline
func MonadChainFirstResultK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B]) ReaderIOResult[R, A] {
func MonadChainFirstResultK[R, A, B any](ma ReaderIOResult[R, A], f result.Kleisli[A, B]) ReaderIOResult[R, A] {
return RIOE.MonadChainFirstEitherK(ma, f)
}
//go:inline
func MonadTapResultK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B]) ReaderIOResult[R, A] {
func MonadTapResultK[R, A, B any](ma ReaderIOResult[R, A], f result.Kleisli[A, B]) ReaderIOResult[R, A] {
return RIOE.MonadTapEitherK(ma, f)
}
@@ -184,12 +185,12 @@ func MonadTapResultK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B])
// This is the curried version of MonadChainFirstEitherK.
//
//go:inline
func ChainFirstResultK[R, A, B any](f func(A) Result[B]) Operator[R, A, A] {
func ChainFirstResultK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, A] {
return RIOE.ChainFirstEitherK[R](f)
}
//go:inline
func TapResultK[R, A, B any](f func(A) Result[B]) Operator[R, A, A] {
func TapResultK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, A] {
return RIOE.TapEitherK[R](f)
}
@@ -230,17 +231,17 @@ func TapReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, A] {
}
//go:inline
func ChainReaderOptionK[R, A, B any](onNone func() error) func(readeroption.Kleisli[R, A, B]) Operator[R, A, B] {
func ChainReaderOptionK[R, A, B any](onNone Lazy[error]) func(readeroption.Kleisli[R, A, B]) Operator[R, A, B] {
return RIOE.ChainReaderOptionK[R, A, B](onNone)
}
//go:inline
func ChainFirstReaderOptionK[R, A, B any](onNone func() error) func(readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
func ChainFirstReaderOptionK[R, A, B any](onNone Lazy[error]) func(readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
return RIOE.ChainFirstReaderOptionK[R, A, B](onNone)
}
//go:inline
func TapReaderOptionK[R, A, B any](onNone func() error) func(readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
func TapReaderOptionK[R, A, B any](onNone Lazy[error]) func(readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
return RIOE.TapReaderOptionK[R, A, B](onNone)
}
@@ -421,7 +422,7 @@ func TapIOK[R, A, B any](f func(A) IO[B]) Operator[R, A, A] {
// If the Option is None, the provided error function is called to produce the error value.
//
//go:inline
func ChainOptionK[R, A, B any](onNone func() error) func(func(A) Option[B]) Operator[R, A, B] {
func ChainOptionK[R, A, B any](onNone Lazy[error]) func(func(A) Option[B]) Operator[R, A, B] {
return RIOE.ChainOptionK[R, A, B](onNone)
}
@@ -619,7 +620,7 @@ func Asks[R, A any](r Reader[R, A]) ReaderIOResult[R, A] {
// If the Option is None, the provided function is called to produce the error.
//
//go:inline
func FromOption[R, A any](onNone func() error) Kleisli[R, Option[A], A] {
func FromOption[R, A any](onNone Lazy[error]) Kleisli[R, Option[A], A] {
return RIOE.FromOption[R, A](onNone)
}

View File

@@ -19,6 +19,7 @@ import (
"github.com/IBM/fp-go/v2/consumer"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
@@ -122,4 +123,6 @@ type (
// Predicate represents a function that tests a value of type A and returns a boolean.
Predicate[A any] = predicate.Predicate[A]
Void = function.Void
)

262
v2/result/filterable.go Normal file
View File

@@ -0,0 +1,262 @@
// 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 result provides filterable operations for Result types.
//
// This package implements the Fantasy Land Filterable specification:
// https://github.com/fantasyland/fantasy-land#filterable
//
// Since Result[A] is an alias for Either[error, A], these functions are
// thin wrappers around the corresponding either package functions, specialized
// for the common case where the error type is Go's built-in error interface.
package result
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/option"
)
// Partition separates a [Result] value into a [Pair] based on a predicate function.
// It returns a function that takes a Result and produces a Pair of Result values,
// where the first element contains values that fail the predicate and the second
// contains values that pass the predicate.
//
// This function implements the Filterable specification's partition operation:
// https://github.com/fantasyland/fantasy-land#filterable
//
// The behavior is as follows:
// - If the input is an error (Left), both elements of the resulting Pair will be the same error
// - If the input is Ok (Right) and the predicate returns true, the result is (Err(empty), Ok(value))
// - If the input is Ok (Right) and the predicate returns false, the result is (Ok(value), Err(empty))
//
// Parameters:
// - p: A predicate function that tests values of type A
// - empty: The default error to use when creating error Results for partitioning
//
// Returns:
//
// A function that takes a Result[A] and returns a Pair where:
// - First element: Result values that fail the predicate (or original error)
// - Second element: Result values that pass the predicate (or original error)
//
// Example:
//
// import (
// R "github.com/IBM/fp-go/v2/result"
// N "github.com/IBM/fp-go/v2/number"
// P "github.com/IBM/fp-go/v2/pair"
// "errors"
// )
//
// // Partition positive and non-positive numbers
// isPositive := N.MoreThan(0)
// partition := R.Partition(isPositive, errors.New("not positive"))
//
// // Ok value that passes predicate
// result1 := partition(R.Of(5))
// // result1 = Pair(Err("not positive"), Ok(5))
//
// // Ok value that fails predicate
// result2 := partition(R.Of(-3))
// // result2 = Pair(Ok(-3), Err("not positive"))
//
// // Error passes through unchanged in both positions
// result3 := partition(R.Error[int](errors.New("original error")))
// // result3 = Pair(Err("original error"), Err("original error"))
//
//go:inline
func Partition[A any](p Predicate[A], empty error) func(Result[A]) Pair[Result[A], Result[A]] {
return either.Partition(p, empty)
}
// Filter creates a filtering operation for [Result] values based on a predicate function.
// It returns a function that takes a Result and produces a Result, where Ok values
// that fail the predicate are converted to error Results with the provided error.
//
// This function implements the Filterable specification's filter operation:
// https://github.com/fantasyland/fantasy-land#filterable
//
// The behavior is as follows:
// - If the input is an error, it passes through unchanged
// - If the input is Ok and the predicate returns true, the Ok value passes through unchanged
// - If the input is Ok and the predicate returns false, it's converted to Err(empty)
//
// Parameters:
// - p: A predicate function that tests values of type A
// - empty: The default error to use when filtering out Ok values that fail the predicate
//
// Returns:
//
// An Operator function that takes a Result[A] and returns a Result[A] where:
// - Error values pass through unchanged
// - Ok values that pass the predicate remain as Ok
// - Ok values that fail the predicate become Err(empty)
//
// Example:
//
// import (
// R "github.com/IBM/fp-go/v2/result"
// N "github.com/IBM/fp-go/v2/number"
// "errors"
// )
//
// // Filter to keep only positive numbers
// isPositive := N.MoreThan(0)
// filterPositive := R.Filter(isPositive, errors.New("not positive"))
//
// // Ok value that passes predicate - remains Ok
// result1 := filterPositive(R.Of(5))
// // result1 = Ok(5)
//
// // Ok value that fails predicate - becomes Err
// result2 := filterPositive(R.Of(-3))
// // result2 = Err("not positive")
//
// // Error passes through unchanged
// result3 := filterPositive(R.Error[int](errors.New("original error")))
// // result3 = Err("original error")
//
// // Chaining filters
// isEven := func(n int) bool { return n%2 == 0 }
// filterEven := R.Filter(isEven, errors.New("not even"))
//
// result4 := filterEven(filterPositive(R.Of(4)))
// // result4 = Ok(4) - passes both filters
//
// result5 := filterEven(filterPositive(R.Of(3)))
// // result5 = Err("not even") - passes first, fails second
//
//go:inline
func Filter[A any](p Predicate[A], empty error) Operator[A, A] {
return either.Filter(p, empty)
}
// FilterMap combines filtering and mapping operations for [Result] values using an [Option]-returning function.
// It returns a function that takes a Result[A] and produces a Result[B], where Ok values
// are transformed by applying the function f. If f returns Some(B), the result is Ok(B). If f returns
// None, the result is Err(empty).
//
// This function implements the Filterable specification's filterMap operation:
// https://github.com/fantasyland/fantasy-land#filterable
//
// The behavior is as follows:
// - If the input is an error, it passes through with its error value preserved
// - If the input is Ok and f returns Some(B), the result is Ok(B)
// - If the input is Ok and f returns None, the result is Err(empty)
//
// Parameters:
// - f: An Option Kleisli function that transforms values of type A to Option[B]
// - empty: The default error to use when f returns None
//
// Returns:
//
// An Operator function that takes a Result[A] and returns a Result[B] where:
// - Error values pass through with error preserved
// - Ok values are transformed by f: Some(B) becomes Ok(B), None becomes Err(empty)
//
// Example:
//
// import (
// R "github.com/IBM/fp-go/v2/result"
// O "github.com/IBM/fp-go/v2/option"
// "errors"
// "strconv"
// )
//
// // Parse string to int, filtering out invalid values
// parseInt := func(s string) O.Option[int] {
// if n, err := strconv.Atoi(s); err == nil {
// return O.Some(n)
// }
// return O.None[int]()
// }
// filterMapInt := R.FilterMap(parseInt, errors.New("invalid number"))
//
// // Valid number string - transforms to Ok(int)
// result1 := filterMapInt(R.Of("42"))
// // result1 = Ok(42)
//
// // Invalid number string - becomes Err
// result2 := filterMapInt(R.Of("abc"))
// // result2 = Err("invalid number")
//
// // Error passes through with error preserved
// result3 := filterMapInt(R.Error[string](errors.New("original error")))
// // result3 = Err("original error")
//
//go:inline
func FilterMap[A, B any](f option.Kleisli[A, B], empty error) Operator[A, B] {
return either.FilterMap(f, empty)
}
// PartitionMap separates and transforms a [Result] value into a [Pair] of Result values using a mapping function.
// It returns a function that takes a Result[A] and produces a Pair of Result values, where the mapping
// function f transforms the Ok value into Either[B, C]. The result is partitioned based on whether f
// produces a Left or Right value.
//
// This function implements the Filterable specification's partitionMap operation:
// https://github.com/fantasyland/fantasy-land#filterable
//
// The behavior is as follows:
// - If the input is an error, both elements of the resulting Pair will be errors with the original error
// - If the input is Ok and f returns Left(B), the result is (Ok(B), Err(empty))
// - If the input is Ok and f returns Right(C), the result is (Err(empty), Ok(C))
//
// Parameters:
// - f: A Kleisli function that transforms values of type A to Either[B, C]
// - empty: The default error to use when creating error Results for partitioning
//
// Returns:
//
// A function that takes a Result[A] and returns a Pair[Result[B], Result[C]] where:
// - If input is error: (Err(original_error), Err(original_error))
// - If f returns Left(B): (Ok(B), Err(empty))
// - If f returns Right(C): (Err(empty), Ok(C))
//
// Example:
//
// import (
// R "github.com/IBM/fp-go/v2/result"
// E "github.com/IBM/fp-go/v2/either"
// P "github.com/IBM/fp-go/v2/pair"
// "errors"
// "strconv"
// )
//
// // Classify and transform numbers: negative -> error message, positive -> squared value
// classifyNumber := func(n int) E.Either[string, int] {
// if n < 0 {
// return E.Left[int]("negative: " + strconv.Itoa(n))
// }
// return E.Right[string](n * n)
// }
// partitionMap := R.PartitionMap(classifyNumber, errors.New("not classified"))
//
// // Positive number - goes to right side as squared value
// result1 := partitionMap(R.Of(5))
// // result1 = Pair(Err("not classified"), Ok(25))
//
// // Negative number - goes to left side with error message
// result2 := partitionMap(R.Of(-3))
// // result2 = Pair(Ok("negative: -3"), Err("not classified"))
//
// // Original error - appears in both positions
// result3 := partitionMap(R.Error[int](errors.New("original error")))
// // result3 = Pair(Err("original error"), Err("original error"))
//
//go:inline
func PartitionMap[A, B, C any](f either.Kleisli[B, A, C], empty error) func(Result[A]) Pair[Result[B], Result[C]] {
return either.PartitionMap(f, empty)
}

View File

@@ -0,0 +1,689 @@
// 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 result
import (
"errors"
"math"
"strconv"
"testing"
E "github.com/IBM/fp-go/v2/either"
N "github.com/IBM/fp-go/v2/number"
O "github.com/IBM/fp-go/v2/option"
P "github.com/IBM/fp-go/v2/pair"
"github.com/stretchr/testify/assert"
)
func TestPartition(t *testing.T) {
t.Run("Ok value that passes predicate", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
partition := Partition(isPositive, errors.New("not positive"))
input := Of(5)
// Act
result := partition(input)
left, right := P.Unpack(result)
// Assert
assert.True(t, IsLeft(left), "left should be error")
assert.True(t, IsRight(right), "right should be Ok")
rightVal, _ := Unwrap(right)
assert.Equal(t, 5, rightVal)
})
t.Run("Ok value that fails predicate", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
partition := Partition(isPositive, errors.New("not positive"))
input := Of(-3)
// Act
result := partition(input)
left, right := P.Unpack(result)
// Assert
assert.True(t, IsRight(left), "left should be Ok (failed predicate)")
assert.True(t, IsLeft(right), "right should be error")
leftVal, _ := Unwrap(left)
assert.Equal(t, -3, leftVal)
})
t.Run("Ok value at boundary (zero)", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
partition := Partition(isPositive, errors.New("not positive"))
input := Of(0)
// Act
result := partition(input)
left, right := P.Unpack(result)
// Assert
assert.True(t, IsRight(left), "left should be Ok (zero fails predicate)")
assert.True(t, IsLeft(right), "right should be error")
leftVal, _ := Unwrap(left)
assert.Equal(t, 0, leftVal)
})
t.Run("Error passes through unchanged", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
partition := Partition(isPositive, errors.New("not positive"))
originalError := errors.New("original error")
input := Left[int](originalError)
// Act
result := partition(input)
left, right := P.Unpack(result)
// Assert
assert.True(t, IsLeft(left), "left should be error")
assert.True(t, IsLeft(right), "right should be error")
_, leftErr := Unwrap(left)
_, rightErr := Unwrap(right)
assert.Equal(t, originalError, leftErr)
assert.Equal(t, originalError, rightErr)
})
t.Run("String predicate - even length strings", func(t *testing.T) {
// Arrange
isEvenLength := func(s string) bool { return len(s)%2 == 0 }
partition := Partition(isEvenLength, errors.New("odd length"))
// Act & Assert - passes predicate
result1 := partition(Of("test"))
left1, right1 := P.Unpack(result1)
assert.True(t, IsLeft(left1))
assert.True(t, IsRight(right1))
rightVal1, _ := Unwrap(right1)
assert.Equal(t, "test", rightVal1)
// Act & Assert - fails predicate
result2 := partition(Of("hello"))
left2, right2 := P.Unpack(result2)
assert.True(t, IsRight(left2))
assert.True(t, IsLeft(right2))
leftVal2, _ := Unwrap(left2)
assert.Equal(t, "hello", leftVal2)
})
t.Run("Complex type predicate - struct field check", func(t *testing.T) {
// Arrange
type Person struct {
Name string
Age int
}
isAdult := func(p Person) bool { return p.Age >= 18 }
partition := Partition(isAdult, errors.New("minor"))
// Act & Assert - adult passes
adult := Person{Name: "Alice", Age: 25}
result1 := partition(Of(adult))
left1, right1 := P.Unpack(result1)
assert.True(t, IsLeft(left1))
assert.True(t, IsRight(right1))
rightVal1, _ := Unwrap(right1)
assert.Equal(t, adult, rightVal1)
// Act & Assert - minor fails
minor := Person{Name: "Bob", Age: 15}
result2 := partition(Of(minor))
left2, right2 := P.Unpack(result2)
assert.True(t, IsRight(left2))
assert.True(t, IsLeft(right2))
leftVal2, _ := Unwrap(left2)
assert.Equal(t, minor, leftVal2)
})
}
func TestFilter(t *testing.T) {
t.Run("Ok value that passes predicate", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
filter := Filter(isPositive, errors.New("not positive"))
input := Of(5)
// Act
result := filter(input)
// Assert
assert.True(t, IsRight(result), "result should be Ok")
val, _ := Unwrap(result)
assert.Equal(t, 5, val)
})
t.Run("Ok value that fails predicate", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
filter := Filter(isPositive, errors.New("not positive"))
input := Of(-3)
// Act
result := filter(input)
// Assert
assert.True(t, IsLeft(result), "result should be error")
_, err := Unwrap(result)
assert.Equal(t, "not positive", err.Error())
})
t.Run("Ok value at boundary (zero)", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
filter := Filter(isPositive, errors.New("not positive"))
input := Of(0)
// Act
result := filter(input)
// Assert
assert.True(t, IsLeft(result), "zero should fail predicate")
_, err := Unwrap(result)
assert.Equal(t, "not positive", err.Error())
})
t.Run("Error passes through unchanged", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
filter := Filter(isPositive, errors.New("not positive"))
originalError := errors.New("original error")
input := Left[int](originalError)
// Act
result := filter(input)
// Assert
assert.True(t, IsLeft(result), "result should be error")
_, err := Unwrap(result)
assert.Equal(t, originalError, err)
})
t.Run("Chaining multiple filters", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
isEven := func(n int) bool { return n%2 == 0 }
filterPositive := Filter(isPositive, errors.New("not positive"))
filterEven := Filter(isEven, errors.New("not even"))
// Act & Assert - passes both filters
result1 := filterEven(filterPositive(Of(4)))
assert.True(t, IsRight(result1))
val1, _ := Unwrap(result1)
assert.Equal(t, 4, val1)
// Act & Assert - passes first, fails second
result2 := filterEven(filterPositive(Of(3)))
assert.True(t, IsLeft(result2))
_, err2 := Unwrap(result2)
assert.Equal(t, "not even", err2.Error())
// Act & Assert - fails first filter
result3 := filterEven(filterPositive(Of(-2)))
assert.True(t, IsLeft(result3))
_, err3 := Unwrap(result3)
assert.Equal(t, "not positive", err3.Error())
// Act & Assert - error passes through both
originalErr := errors.New("original")
result4 := filterEven(filterPositive(Left[int](originalErr)))
assert.True(t, IsLeft(result4))
_, err4 := Unwrap(result4)
assert.Equal(t, originalErr, err4)
})
t.Run("Filter preserves error", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
filter := Filter(isPositive, errors.New("default error"))
// Act - error with different message
originalError := errors.New("server error")
result := filter(Left[int](originalError))
// Assert - original error preserved
assert.True(t, IsLeft(result))
_, err := Unwrap(result)
assert.Equal(t, originalError, err)
})
}
func TestFilterMap(t *testing.T) {
t.Run("Ok value with Some result", func(t *testing.T) {
// Arrange
parseInt := func(s string) O.Option[int] {
if n, err := strconv.Atoi(s); err == nil {
return O.Some(n)
}
return O.None[int]()
}
filterMap := FilterMap(parseInt, errors.New("invalid number"))
input := Of("42")
// Act
result := filterMap(input)
// Assert
assert.True(t, IsRight(result), "result should be Ok")
val, _ := Unwrap(result)
assert.Equal(t, 42, val)
})
t.Run("Ok value with None result", func(t *testing.T) {
// Arrange
parseInt := func(s string) O.Option[int] {
if n, err := strconv.Atoi(s); err == nil {
return O.Some(n)
}
return O.None[int]()
}
filterMap := FilterMap(parseInt, errors.New("invalid number"))
input := Of("abc")
// Act
result := filterMap(input)
// Assert
assert.True(t, IsLeft(result), "result should be error")
_, err := Unwrap(result)
assert.Equal(t, "invalid number", err.Error())
})
t.Run("Error passes through", func(t *testing.T) {
// Arrange
parseInt := func(s string) O.Option[int] {
if n, err := strconv.Atoi(s); err == nil {
return O.Some(n)
}
return O.None[int]()
}
filterMap := FilterMap(parseInt, errors.New("invalid number"))
originalError := errors.New("original error")
input := Left[string](originalError)
// Act
result := filterMap(input)
// Assert
assert.True(t, IsLeft(result), "result should be error")
_, err := Unwrap(result)
assert.Equal(t, originalError, err)
})
t.Run("Extract optional field from struct", func(t *testing.T) {
// Arrange
type Person struct {
Name string
Email O.Option[string]
}
extractEmail := func(p Person) O.Option[string] { return p.Email }
filterMap := FilterMap(extractEmail, errors.New("no email"))
// Act & Assert - has email
result1 := filterMap(Of(Person{Name: "Alice", Email: O.Some("alice@example.com")}))
assert.True(t, IsRight(result1))
val1, _ := Unwrap(result1)
assert.Equal(t, "alice@example.com", val1)
// Act & Assert - no email
result2 := filterMap(Of(Person{Name: "Bob", Email: O.None[string]()}))
assert.True(t, IsLeft(result2))
_, err2 := Unwrap(result2)
assert.Equal(t, "no email", err2.Error())
})
t.Run("Transform and filter numbers", func(t *testing.T) {
// Arrange
sqrtIfPositive := func(n int) O.Option[float64] {
if n >= 0 {
return O.Some(math.Sqrt(float64(n)))
}
return O.None[float64]()
}
filterMap := FilterMap(sqrtIfPositive, errors.New("negative number"))
// Act & Assert - positive number
result1 := filterMap(Of(16))
assert.True(t, IsRight(result1))
val1, _ := Unwrap(result1)
assert.Equal(t, 4.0, val1)
// Act & Assert - negative number
result2 := filterMap(Of(-4))
assert.True(t, IsLeft(result2))
_, err2 := Unwrap(result2)
assert.Equal(t, "negative number", err2.Error())
// Act & Assert - zero
result3 := filterMap(Of(0))
assert.True(t, IsRight(result3))
val3, _ := Unwrap(result3)
assert.Equal(t, 0.0, val3)
})
t.Run("Chain multiple FilterMap operations", func(t *testing.T) {
// Arrange
parseInt := func(s string) O.Option[int] {
if n, err := strconv.Atoi(s); err == nil {
return O.Some(n)
}
return O.None[int]()
}
doubleIfEven := func(n int) O.Option[int] {
if n%2 == 0 {
return O.Some(n * 2)
}
return O.None[int]()
}
filterMap1 := FilterMap(parseInt, errors.New("invalid number"))
filterMap2 := FilterMap(doubleIfEven, errors.New("not even"))
// Act & Assert - valid even number
result1 := filterMap2(filterMap1(Of("4")))
assert.True(t, IsRight(result1))
val1, _ := Unwrap(result1)
assert.Equal(t, 8, val1)
// Act & Assert - valid odd number
result2 := filterMap2(filterMap1(Of("3")))
assert.True(t, IsLeft(result2))
_, err2 := Unwrap(result2)
assert.Equal(t, "not even", err2.Error())
// Act & Assert - invalid number
result3 := filterMap2(filterMap1(Of("abc")))
assert.True(t, IsLeft(result3))
_, err3 := Unwrap(result3)
assert.Equal(t, "invalid number", err3.Error())
})
}
func TestPartitionMap(t *testing.T) {
t.Run("Ok value that maps to Left", func(t *testing.T) {
// Arrange
classifyNumber := func(n int) E.Either[string, int] {
if n < 0 {
return E.Left[int]("negative: " + strconv.Itoa(n))
}
return E.Right[string](n * n)
}
partitionMap := PartitionMap(classifyNumber, errors.New("not classified"))
input := Of(-3)
// Act
result := partitionMap(input)
left, right := P.Unpack(result)
// Assert
assert.True(t, IsRight(left), "left should be Ok (contains error from f)")
assert.True(t, IsLeft(right), "right should be error")
leftVal, _ := Unwrap(left)
assert.Equal(t, "negative: -3", leftVal)
})
t.Run("Ok value that maps to Right", func(t *testing.T) {
// Arrange
classifyNumber := func(n int) E.Either[string, int] {
if n < 0 {
return E.Left[int]("negative: " + strconv.Itoa(n))
}
return E.Right[string](n * n)
}
partitionMap := PartitionMap(classifyNumber, errors.New("not classified"))
input := Of(5)
// Act
result := partitionMap(input)
left, right := P.Unpack(result)
// Assert
assert.True(t, IsLeft(left), "left should be error")
assert.True(t, IsRight(right), "right should be Ok (contains value from f)")
rightVal, _ := Unwrap(right)
assert.Equal(t, 25, rightVal)
})
t.Run("Error passes through to both sides", func(t *testing.T) {
// Arrange
classifyNumber := func(n int) E.Either[string, int] {
if n < 0 {
return E.Left[int]("negative")
}
return E.Right[string](n * n)
}
partitionMap := PartitionMap(classifyNumber, errors.New("not classified"))
originalError := errors.New("original error")
input := Left[int](originalError)
// Act
result := partitionMap(input)
left, right := P.Unpack(result)
// Assert
assert.True(t, IsLeft(left), "left should be error")
assert.True(t, IsLeft(right), "right should be error")
_, leftErr := Unwrap(left)
_, rightErr := Unwrap(right)
assert.Equal(t, originalError, leftErr)
assert.Equal(t, originalError, rightErr)
})
t.Run("Validate and transform user input", func(t *testing.T) {
// Arrange
type ValidationError struct {
Field string
Message string
}
type User struct {
Name string
Age int
}
validateUser := func(input map[string]string) E.Either[ValidationError, User] {
name, hasName := input["name"]
ageStr, hasAge := input["age"]
if !hasName {
return E.Left[User](ValidationError{"name", "missing"})
}
if !hasAge {
return E.Left[User](ValidationError{"age", "missing"})
}
age, err := strconv.Atoi(ageStr)
if err != nil {
return E.Left[User](ValidationError{"age", "invalid"})
}
return E.Right[ValidationError](User{name, age})
}
partitionMap := PartitionMap(validateUser, errors.New("not processed"))
// Act & Assert - valid input
validInput := map[string]string{"name": "Alice", "age": "30"}
result1 := partitionMap(Of(validInput))
left1, right1 := P.Unpack(result1)
assert.True(t, IsLeft(left1))
assert.True(t, IsRight(right1))
rightVal1, _ := Unwrap(right1)
assert.Equal(t, User{"Alice", 30}, rightVal1)
// Act & Assert - invalid input (missing age)
invalidInput := map[string]string{"name": "Bob"}
result2 := partitionMap(Of(invalidInput))
left2, right2 := P.Unpack(result2)
assert.True(t, IsRight(left2))
assert.True(t, IsLeft(right2))
leftVal2, _ := Unwrap(left2)
assert.Equal(t, ValidationError{"age", "missing"}, leftVal2)
})
t.Run("Classify strings by length", func(t *testing.T) {
// Arrange
classifyString := func(s string) E.Either[string, int] {
if len(s) < 5 {
return E.Left[int]("too short: " + s)
}
return E.Right[string](len(s))
}
partitionMap := PartitionMap(classifyString, errors.New("not classified"))
// Act & Assert - short string
result1 := partitionMap(Of("hi"))
left1, right1 := P.Unpack(result1)
assert.True(t, IsRight(left1))
assert.True(t, IsLeft(right1))
leftVal1, _ := Unwrap(left1)
assert.Equal(t, "too short: hi", leftVal1)
// Act & Assert - long string
result2 := partitionMap(Of("hello world"))
left2, right2 := P.Unpack(result2)
assert.True(t, IsLeft(left2))
assert.True(t, IsRight(right2))
rightVal2, _ := Unwrap(right2)
assert.Equal(t, 11, rightVal2)
})
}
// Benchmark tests
func BenchmarkPartition(b *testing.B) {
isPositive := N.MoreThan(0)
partition := Partition(isPositive, errors.New("not positive"))
input := Of(42)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = partition(input)
}
}
func BenchmarkPartitionError(b *testing.B) {
isPositive := N.MoreThan(0)
partition := Partition(isPositive, errors.New("not positive"))
input := Left[int](errors.New("error"))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = partition(input)
}
}
func BenchmarkFilter(b *testing.B) {
isPositive := N.MoreThan(0)
filter := Filter(isPositive, errors.New("not positive"))
input := Of(42)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = filter(input)
}
}
func BenchmarkFilterError(b *testing.B) {
isPositive := N.MoreThan(0)
filter := Filter(isPositive, errors.New("not positive"))
input := Left[int](errors.New("error"))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = filter(input)
}
}
func BenchmarkFilterChained(b *testing.B) {
isPositive := N.MoreThan(0)
isEven := func(n int) bool { return n%2 == 0 }
filterPositive := Filter(isPositive, errors.New("not positive"))
filterEven := Filter(isEven, errors.New("not even"))
input := Of(42)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = filterEven(filterPositive(input))
}
}
func BenchmarkFilterMap(b *testing.B) {
parseInt := func(s string) O.Option[int] {
if n, err := strconv.Atoi(s); err == nil {
return O.Some(n)
}
return O.None[int]()
}
filterMap := FilterMap(parseInt, errors.New("invalid"))
input := Of("42")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = filterMap(input)
}
}
func BenchmarkFilterMapError(b *testing.B) {
parseInt := func(s string) O.Option[int] {
if n, err := strconv.Atoi(s); err == nil {
return O.Some(n)
}
return O.None[int]()
}
filterMap := FilterMap(parseInt, errors.New("invalid"))
input := Left[string](errors.New("error"))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = filterMap(input)
}
}
func BenchmarkPartitionMap(b *testing.B) {
classify := func(n int) E.Either[string, int] {
if n < 0 {
return E.Left[int]("negative")
}
return E.Right[string](n * n)
}
partitionMap := PartitionMap(classify, errors.New("not classified"))
input := Of(42)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = partitionMap(input)
}
}
func BenchmarkPartitionMapError(b *testing.B) {
classify := func(n int) E.Either[string, int] {
if n < 0 {
return E.Left[int]("negative")
}
return E.Right[string](n * n)
}
partitionMap := PartitionMap(classify, errors.New("not classified"))
input := Left[int](errors.New("error"))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = partitionMap(input)
}
}

View File

@@ -22,6 +22,7 @@ import (
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/reader"
)
@@ -61,4 +62,6 @@ type (
// Predicate represents a function that tests a value of type A and returns a boolean.
// It's commonly used for filtering and conditional operations.
Predicate[A any] = predicate.Predicate[A]
Pair[L, R any] = pair.Pair[L, R]
)