mirror of
https://github.com/IBM/fp-go.git
synced 2026-02-24 12:57:26 +02:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47727fd514 | ||
|
|
ece7d088ea | ||
|
|
13d25eca32 | ||
|
|
a68e32308d | ||
|
|
61b948425b | ||
|
|
a276f3acff | ||
|
|
8c656a4297 | ||
|
|
bd9a642e93 | ||
|
|
3b55cae265 | ||
|
|
1472fa5a50 |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -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:
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"mcpServers":{}}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
351
v2/either/filterable.go
Normal 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
1433
v2/either/filterable_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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
158
v2/iterator/iter/option.go
Normal 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)
|
||||
}
|
||||
387
v2/iterator/iter/option_test.go
Normal file
387
v2/iterator/iter/option_test.go
Normal 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
99
v2/llms.txt
Normal 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
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
480
v2/optics/codec/alt.go
Normal file
480
v2/optics/codec/alt.go
Normal file
@@ -0,0 +1,480 @@
|
||||
// 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 codec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// validateAlt creates a validation function that tries the first codec's validation,
|
||||
// and if it fails, tries the second codec's validation as a fallback.
|
||||
//
|
||||
// This is an internal helper function that implements the Alternative pattern for
|
||||
// codec validation. It combines two codec validators using the validate.Alt operation,
|
||||
// which provides error recovery and fallback logic.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The target type that both codecs decode to
|
||||
// - O: The output type that both codecs encode to
|
||||
// - I: The input type that both codecs decode from
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - first: The primary codec whose validation is tried first
|
||||
// - second: A lazy codec that serves as the fallback. It's only evaluated if the
|
||||
// first validation fails.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A Validate[I, A] function that tries the first codec's validation, falling back
|
||||
// to the second if needed. If both fail, errors from both are aggregated.
|
||||
//
|
||||
// # Behavior
|
||||
//
|
||||
// - **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
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - The second codec is lazily evaluated for efficiency
|
||||
// - This function is used internally by MonadAlt and Alt
|
||||
// - The validation context is threaded through both validators
|
||||
// - Errors are accumulated using the validation error monoid
|
||||
func validateAlt[A, O, I any](
|
||||
first Type[A, O, I],
|
||||
second Lazy[Type[A, O, I]],
|
||||
) Validate[I, A] {
|
||||
|
||||
return F.Pipe1(
|
||||
first.Validate,
|
||||
validate.Alt(F.Pipe1(
|
||||
second,
|
||||
lazy.Map(F.Flip(reader.Curry(Type[A, O, I].Validate))),
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
// MonadAlt creates a new codec that tries the first codec, and if it fails during
|
||||
// validation, tries the second codec as a fallback.
|
||||
//
|
||||
// This function implements the Alternative typeclass pattern for codecs, enabling
|
||||
// "try this codec, or else try that codec" logic. It's particularly useful for:
|
||||
// - Handling multiple valid input formats
|
||||
// - Providing backward compatibility with legacy formats
|
||||
// - Implementing graceful degradation in parsing
|
||||
// - Supporting union types or polymorphic data
|
||||
//
|
||||
// The resulting codec uses the first codec's encoder and combines both validators
|
||||
// using the Alternative pattern. If both validations fail, errors from both are
|
||||
// aggregated for comprehensive error reporting.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The target type that both codecs decode to
|
||||
// - O: The output type that both codecs encode to
|
||||
// - I: The input type that both codecs decode from
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - first: The primary codec to try first. Its encoder is used for the result.
|
||||
// - second: A lazy codec that serves as the fallback. It's only evaluated if the
|
||||
// first validation fails.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A new Type[A, O, I] that combines both codecs with Alternative semantics.
|
||||
//
|
||||
// # Behavior
|
||||
//
|
||||
// **Validation**:
|
||||
// - **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
|
||||
//
|
||||
// **Encoding**:
|
||||
// - Always uses the first codec's encoder
|
||||
// - This assumes both codecs encode to the same output format
|
||||
//
|
||||
// **Type Checking**:
|
||||
// - Uses the generic Is[A]() type checker
|
||||
// - Validates that values are of type A
|
||||
//
|
||||
// # Example: Multiple Input Formats
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/optics/codec"
|
||||
// )
|
||||
//
|
||||
// // Accept integers as either strings or numbers
|
||||
// intFromString := codec.IntFromString()
|
||||
// intFromNumber := codec.Int()
|
||||
//
|
||||
// // Try parsing as string first, fall back to number
|
||||
// flexibleInt := codec.MonadAlt(
|
||||
// intFromString,
|
||||
// func() codec.Type[int, any, any] { return intFromNumber },
|
||||
// )
|
||||
//
|
||||
// // Can now decode both "42" and 42
|
||||
// result1 := flexibleInt.Decode("42") // Success(42)
|
||||
// result2 := flexibleInt.Decode(42) // Success(42)
|
||||
//
|
||||
// # Example: Backward Compatibility
|
||||
//
|
||||
// // Support both old and new configuration formats
|
||||
// newConfigCodec := codec.Struct(/* new format */)
|
||||
// oldConfigCodec := codec.Struct(/* old format */)
|
||||
//
|
||||
// // Try new format first, fall back to old format
|
||||
// configCodec := codec.MonadAlt(
|
||||
// newConfigCodec,
|
||||
// func() codec.Type[Config, any, any] { return oldConfigCodec },
|
||||
// )
|
||||
//
|
||||
// // Automatically handles both formats
|
||||
// config := configCodec.Decode(input)
|
||||
//
|
||||
// # Example: Error Aggregation
|
||||
//
|
||||
// // Both validations will fail for invalid input
|
||||
// result := flexibleInt.Decode("not a number")
|
||||
// // Result contains errors from both string and number parsing attempts
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - The second codec is lazily evaluated for efficiency
|
||||
// - First success short-circuits evaluation (second not called)
|
||||
// - Errors are aggregated when both fail
|
||||
// - The resulting codec's name is "Alt[<first codec name>]"
|
||||
// - Both codecs must have compatible input and output types
|
||||
// - The first codec's encoder is always used
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Alt: The curried, point-free version
|
||||
// - validate.MonadAlt: The underlying validation operation
|
||||
// - Either: For codecs that decode to Either[L, R] types
|
||||
func MonadAlt[A, O, I any](first Type[A, O, I], second Lazy[Type[A, O, I]]) Type[A, O, I] {
|
||||
return MakeType(
|
||||
fmt.Sprintf("Alt[%s]", first.Name()),
|
||||
Is[A](),
|
||||
validateAlt(first, second),
|
||||
first.Encode,
|
||||
)
|
||||
}
|
||||
|
||||
// Alt creates an operator that adds alternative fallback logic to a codec.
|
||||
//
|
||||
// This is the curried, point-free version of MonadAlt. It returns a function that
|
||||
// can be applied to codecs to add fallback behavior. This style is particularly
|
||||
// useful for building codec transformation pipelines using function composition.
|
||||
//
|
||||
// Alt implements the Alternative typeclass pattern, enabling "try this codec, or
|
||||
// else try that codec" logic in a composable way.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The target type that both codecs decode to
|
||||
// - O: The output type that both codecs encode to
|
||||
// - I: The input type that both codecs decode from
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - second: A lazy codec that serves as the fallback. It's only evaluated if the
|
||||
// first codec's validation fails.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// An Operator[A, A, O, I] that transforms codecs by adding alternative fallback logic.
|
||||
// This operator can be applied to any Type[A, O, I] to create a new codec with
|
||||
// fallback behavior.
|
||||
//
|
||||
// # Behavior
|
||||
//
|
||||
// When the returned operator is applied to a codec:
|
||||
// - **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
|
||||
//
|
||||
// # Example: Point-Free Style
|
||||
//
|
||||
// import (
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// "github.com/IBM/fp-go/v2/optics/codec"
|
||||
// )
|
||||
//
|
||||
// // Create a reusable fallback operator
|
||||
// withNumberFallback := codec.Alt(func() codec.Type[int, any, any] {
|
||||
// return codec.Int()
|
||||
// })
|
||||
//
|
||||
// // Apply it to different codecs
|
||||
// flexibleInt1 := withNumberFallback(codec.IntFromString())
|
||||
// flexibleInt2 := withNumberFallback(codec.IntFromHex())
|
||||
//
|
||||
// # Example: Pipeline Composition
|
||||
//
|
||||
// // Build a codec pipeline with multiple fallbacks
|
||||
// flexibleCodec := F.Pipe2(
|
||||
// primaryCodec,
|
||||
// codec.Alt(func() codec.Type[T, O, I] { return fallback1 }),
|
||||
// codec.Alt(func() codec.Type[T, O, I] { return fallback2 }),
|
||||
// )
|
||||
// // Tries primary, then fallback1, then fallback2
|
||||
//
|
||||
// # Example: Reusable Transformations
|
||||
//
|
||||
// // Create a transformation that adds JSON fallback
|
||||
// withJSONFallback := codec.Alt(func() codec.Type[Config, string, string] {
|
||||
// return codec.JSONCodec[Config]()
|
||||
// })
|
||||
//
|
||||
// // Apply to multiple codecs
|
||||
// yamlWithFallback := withJSONFallback(yamlCodec)
|
||||
// tomlWithFallback := withJSONFallback(tomlCodec)
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - This is the point-free style version of MonadAlt
|
||||
// - Useful for building transformation pipelines with F.Pipe
|
||||
// - The second codec is lazily evaluated for efficiency
|
||||
// - First success short-circuits evaluation
|
||||
// - Errors are aggregated when both fail
|
||||
// - Can be composed with other codec operators
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - MonadAlt: The direct application version
|
||||
// - validate.Alt: The underlying validation operation
|
||||
// - F.Pipe: For composing multiple operators
|
||||
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],
|
||||
)
|
||||
}
|
||||
921
v2/optics/codec/alt_test.go
Normal file
921
v2/optics/codec/alt_test.go
Normal file
@@ -0,0 +1,921 @@
|
||||
// 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 codec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestMonadAltBasicFunctionality tests the basic behavior of MonadAlt
|
||||
func TestMonadAltBasicFunctionality(t *testing.T) {
|
||||
t.Run("uses first codec when it succeeds", func(t *testing.T) {
|
||||
// Create two codecs that both work with strings
|
||||
stringCodec := Id[string]()
|
||||
|
||||
// Create another string codec that only accepts uppercase
|
||||
uppercaseOnly := MakeType(
|
||||
"UppercaseOnly",
|
||||
Is[string](),
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
for _, r := range s {
|
||||
if r >= 'a' && r <= 'z' {
|
||||
return validation.FailureWithMessage[string](s, "must be uppercase")(c)
|
||||
}
|
||||
}
|
||||
return validation.Success(s)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
// Create alt codec that tries uppercase first, then any string
|
||||
altCodec := MonadAlt(
|
||||
uppercaseOnly,
|
||||
func() Type[string, string, string] { return stringCodec },
|
||||
)
|
||||
|
||||
// Test with uppercase string - should succeed with first codec
|
||||
result := altCodec.Decode("HELLO")
|
||||
|
||||
assert.True(t, either.IsRight(result), "should successfully decode with first codec")
|
||||
|
||||
value := either.GetOrElse(reader.Of[validation.Errors, string](""))(result)
|
||||
assert.Equal(t, "HELLO", value)
|
||||
})
|
||||
|
||||
t.Run("falls back to second codec when first fails", func(t *testing.T) {
|
||||
// Create a codec that only accepts positive integers
|
||||
positiveInt := MakeType(
|
||||
"PositiveInt",
|
||||
Is[int](),
|
||||
func(i int) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
if i <= 0 {
|
||||
return validation.FailureWithMessage[int](i, "must be positive")(c)
|
||||
}
|
||||
return validation.Success(i)
|
||||
}
|
||||
},
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
// Create a codec that accepts any integer (with same input type)
|
||||
anyInt := MakeType(
|
||||
"AnyInt",
|
||||
Is[int](),
|
||||
func(i int) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.Success(i)
|
||||
}
|
||||
},
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
// Create alt codec
|
||||
altCodec := MonadAlt(
|
||||
positiveInt,
|
||||
func() Type[int, int, int] { return anyInt },
|
||||
)
|
||||
|
||||
// Test with negative number - first fails, second succeeds
|
||||
result := altCodec.Decode(-5)
|
||||
|
||||
assert.True(t, either.IsRight(result), "should successfully decode with second codec")
|
||||
|
||||
value := either.GetOrElse(reader.Of[validation.Errors, int](0))(result)
|
||||
assert.Equal(t, -5, value)
|
||||
})
|
||||
|
||||
t.Run("aggregates errors when both codecs fail", func(t *testing.T) {
|
||||
// Create two codecs that will both fail
|
||||
positiveInt := MakeType(
|
||||
"PositiveInt",
|
||||
Is[int](),
|
||||
func(i int) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
if i <= 0 {
|
||||
return validation.FailureWithMessage[int](i, "must be positive")(c)
|
||||
}
|
||||
return validation.Success(i)
|
||||
}
|
||||
},
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
evenInt := MakeType(
|
||||
"EvenInt",
|
||||
Is[int](),
|
||||
func(i int) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
if i%2 != 0 {
|
||||
return validation.FailureWithMessage[int](i, "must be even")(c)
|
||||
}
|
||||
return validation.Success(i)
|
||||
}
|
||||
},
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
// Create alt codec
|
||||
altCodec := MonadAlt(
|
||||
positiveInt,
|
||||
func() Type[int, int, int] { return evenInt },
|
||||
)
|
||||
|
||||
// Test with -3 (negative and odd) - both should fail
|
||||
result := altCodec.Decode(-3)
|
||||
|
||||
assert.True(t, either.IsLeft(result), "should fail when both codecs fail")
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(int) validation.Errors { return nil },
|
||||
)
|
||||
|
||||
require.NotNil(t, errors)
|
||||
// Should have errors from both validation attempts
|
||||
assert.GreaterOrEqual(t, len(errors), 2, "should have errors from both codecs")
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadAltNaming tests that the codec name is correctly generated
|
||||
func TestMonadAltNaming(t *testing.T) {
|
||||
t.Run("generates correct name", func(t *testing.T) {
|
||||
stringCodec := Id[string]()
|
||||
anotherStringCodec := Id[string]()
|
||||
|
||||
altCodec := MonadAlt(
|
||||
stringCodec,
|
||||
func() Type[string, string, string] { return anotherStringCodec },
|
||||
)
|
||||
|
||||
assert.Equal(t, "Alt[string]", altCodec.Name())
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadAltEncoding tests that encoding uses the first codec's encoder
|
||||
func TestMonadAltEncoding(t *testing.T) {
|
||||
t.Run("uses first codec's encoder", func(t *testing.T) {
|
||||
// Create a codec that encodes ints as strings with prefix
|
||||
prefixedInt := MakeType(
|
||||
"PrefixedInt",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
var n int
|
||||
_, err := fmt.Sscanf(s, "NUM:%d", &n)
|
||||
if err != nil {
|
||||
return validation.FailureWithError[int](s, "expected NUM:n format")(err)(c)
|
||||
}
|
||||
return validation.Success(n)
|
||||
}
|
||||
},
|
||||
func(n int) string {
|
||||
return fmt.Sprintf("NUM:%d", n)
|
||||
},
|
||||
)
|
||||
|
||||
// Create a standard int from string codec
|
||||
standardInt := IntFromString()
|
||||
|
||||
// Create alt codec
|
||||
altCodec := MonadAlt(
|
||||
prefixedInt,
|
||||
func() Type[int, string, string] { return standardInt },
|
||||
)
|
||||
|
||||
// Encode should use first codec's encoder
|
||||
encoded := altCodec.Encode(42)
|
||||
assert.Equal(t, "NUM:42", encoded)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAltOperator tests the curried Alt function
|
||||
func TestAltOperator(t *testing.T) {
|
||||
t.Run("creates reusable operator", func(t *testing.T) {
|
||||
// Create a fallback operator that accepts any string
|
||||
withStringFallback := Alt(func() Type[string, string, string] {
|
||||
return Id[string]()
|
||||
})
|
||||
|
||||
// Create a codec that only accepts "hello"
|
||||
helloOnly := MakeType(
|
||||
"HelloOnly",
|
||||
Is[string](),
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
if s != "hello" {
|
||||
return validation.FailureWithMessage[string](s, "must be 'hello'")(c)
|
||||
}
|
||||
return validation.Success(s)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
// Apply fallback to the codec
|
||||
altCodec := withStringFallback(helloOnly)
|
||||
|
||||
// Test that it works
|
||||
result1 := altCodec.Decode("hello")
|
||||
assert.True(t, either.IsRight(result1))
|
||||
|
||||
result2 := altCodec.Decode("world")
|
||||
assert.True(t, either.IsRight(result2))
|
||||
})
|
||||
|
||||
t.Run("works in pipeline with F.Pipe", func(t *testing.T) {
|
||||
// Create a codec pipeline with multiple fallbacks
|
||||
baseCodec := MakeType(
|
||||
"StrictInt",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
if s != "42" {
|
||||
return validation.FailureWithMessage[int](s, "must be exactly '42'")(c)
|
||||
}
|
||||
return validation.Success(42)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
fallback1 := MakeType(
|
||||
"Fallback1",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
if s != "100" {
|
||||
return validation.FailureWithMessage[int](s, "must be exactly '100'")(c)
|
||||
}
|
||||
return validation.Success(100)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
fallback2 := MakeType(
|
||||
"AnyInt",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return validation.FailureWithError[int](s, "not an integer")(err)(c)
|
||||
}
|
||||
return validation.Success(n)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
// Build pipeline with multiple alternatives
|
||||
pipeline := F.Pipe2(
|
||||
baseCodec,
|
||||
Alt(func() Type[int, string, string] { return fallback1 }),
|
||||
Alt(func() Type[int, string, string] { return fallback2 }),
|
||||
)
|
||||
|
||||
// Test with "42" - should use base codec
|
||||
result1 := pipeline.Decode("42")
|
||||
assert.True(t, either.IsRight(result1))
|
||||
value1 := either.GetOrElse(reader.Of[validation.Errors, int](0))(result1)
|
||||
assert.Equal(t, 42, value1)
|
||||
|
||||
// Test with "100" - should use fallback1
|
||||
result2 := pipeline.Decode("100")
|
||||
assert.True(t, either.IsRight(result2))
|
||||
value2 := either.GetOrElse(reader.Of[validation.Errors, int](0))(result2)
|
||||
assert.Equal(t, 100, value2)
|
||||
|
||||
// Test with "999" - should use fallback2
|
||||
result3 := pipeline.Decode("999")
|
||||
assert.True(t, either.IsRight(result3))
|
||||
value3 := either.GetOrElse(reader.Of[validation.Errors, int](0))(result3)
|
||||
assert.Equal(t, 999, value3)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAltLazyEvaluation tests that the second codec is only evaluated when needed
|
||||
func TestAltLazyEvaluation(t *testing.T) {
|
||||
t.Run("does not evaluate second codec when first succeeds", func(t *testing.T) {
|
||||
evaluated := false
|
||||
|
||||
stringCodec := Id[string]()
|
||||
|
||||
altCodec := MonadAlt(
|
||||
stringCodec,
|
||||
func() Type[string, string, string] {
|
||||
evaluated = true
|
||||
return Id[string]()
|
||||
},
|
||||
)
|
||||
|
||||
// Decode with first codec succeeding
|
||||
result := altCodec.Decode("hello")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
// Second codec should not have been evaluated
|
||||
assert.False(t, evaluated, "second codec should not be evaluated when first succeeds")
|
||||
})
|
||||
|
||||
t.Run("evaluates second codec when first fails", func(t *testing.T) {
|
||||
evaluated := false
|
||||
|
||||
// Create a codec that always fails
|
||||
failingCodec := MakeType(
|
||||
"Failing",
|
||||
Is[string](),
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
return validation.FailureWithMessage[string](s, "always fails")(c)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
altCodec := MonadAlt(
|
||||
failingCodec,
|
||||
func() Type[string, string, string] {
|
||||
evaluated = true
|
||||
return Id[string]()
|
||||
},
|
||||
)
|
||||
|
||||
// Decode with first codec failing
|
||||
result := altCodec.Decode("hello")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
// Second codec should have been evaluated
|
||||
assert.True(t, evaluated, "second codec should be evaluated when first fails")
|
||||
})
|
||||
}
|
||||
|
||||
// TestAltWithComplexTypes tests Alt with more complex codec scenarios
|
||||
func TestAltWithComplexTypes(t *testing.T) {
|
||||
t.Run("works with string length validation", func(t *testing.T) {
|
||||
// Create codec that accepts strings of length 5
|
||||
length5 := MakeType(
|
||||
"Length5",
|
||||
Is[string](),
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
if len(s) != 5 {
|
||||
return validation.FailureWithMessage[string](s, "must be length 5")(c)
|
||||
}
|
||||
return validation.Success(s)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
// Create codec that accepts any string
|
||||
anyString := Id[string]()
|
||||
|
||||
// Create alt codec
|
||||
altCodec := MonadAlt(
|
||||
length5,
|
||||
func() Type[string, string, string] { return anyString },
|
||||
)
|
||||
|
||||
// Test with length 5 - should use first codec
|
||||
result1 := altCodec.Decode("hello")
|
||||
assert.True(t, either.IsRight(result1))
|
||||
|
||||
// Test with different length - should fall back to second codec
|
||||
result2 := altCodec.Decode("hi")
|
||||
assert.True(t, either.IsRight(result2))
|
||||
})
|
||||
}
|
||||
|
||||
// TestAltTypeChecking tests that type checking works correctly
|
||||
func TestAltTypeChecking(t *testing.T) {
|
||||
t.Run("type checking uses generic Is", func(t *testing.T) {
|
||||
stringCodec := Id[string]()
|
||||
anotherStringCodec := Id[string]()
|
||||
|
||||
altCodec := MonadAlt(
|
||||
stringCodec,
|
||||
func() Type[string, string, string] { return anotherStringCodec },
|
||||
)
|
||||
|
||||
// Test Is with valid type
|
||||
result1 := altCodec.Is("hello")
|
||||
assert.True(t, either.IsRight(result1))
|
||||
|
||||
// Test Is with invalid type
|
||||
result2 := altCodec.Is(42)
|
||||
assert.True(t, either.IsLeft(result2))
|
||||
})
|
||||
}
|
||||
|
||||
// TestAltRoundTrip tests encoding and decoding round trips
|
||||
func TestAltRoundTrip(t *testing.T) {
|
||||
t.Run("round-trip with first codec", func(t *testing.T) {
|
||||
stringCodec := Id[string]()
|
||||
anotherStringCodec := Id[string]()
|
||||
|
||||
altCodec := MonadAlt(
|
||||
stringCodec,
|
||||
func() Type[string, string, string] { return anotherStringCodec },
|
||||
)
|
||||
|
||||
original := "hello"
|
||||
|
||||
// Decode
|
||||
decodeResult := altCodec.Decode(original)
|
||||
require.True(t, either.IsRight(decodeResult))
|
||||
|
||||
decoded := either.GetOrElse(reader.Of[validation.Errors, string](""))(decodeResult)
|
||||
|
||||
// Encode
|
||||
encoded := altCodec.Encode(decoded)
|
||||
|
||||
// Verify
|
||||
assert.Equal(t, original, encoded)
|
||||
})
|
||||
|
||||
t.Run("round-trip with second codec", func(t *testing.T) {
|
||||
// Create a codec that only accepts "hello"
|
||||
helloOnly := MakeType(
|
||||
"HelloOnly",
|
||||
Is[string](),
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
if s != "hello" {
|
||||
return validation.FailureWithMessage[string](s, "must be 'hello'")(c)
|
||||
}
|
||||
return validation.Success(s)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
anyString := Id[string]()
|
||||
|
||||
altCodec := MonadAlt(
|
||||
helloOnly,
|
||||
func() Type[string, string, string] { return anyString },
|
||||
)
|
||||
|
||||
original := "world"
|
||||
|
||||
// Decode (will use second codec)
|
||||
decodeResult := altCodec.Decode(original)
|
||||
require.True(t, either.IsRight(decodeResult))
|
||||
|
||||
decoded := either.GetOrElse(reader.Of[validation.Errors, string](""))(decodeResult)
|
||||
|
||||
// Encode (uses first codec's encoder, which is identity)
|
||||
encoded := altCodec.Encode(decoded)
|
||||
|
||||
// Verify
|
||||
assert.Equal(t, original, encoded)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAltErrorMessages tests that error messages are informative
|
||||
func TestAltErrorMessages(t *testing.T) {
|
||||
t.Run("provides detailed error context", func(t *testing.T) {
|
||||
// Create two codecs with specific error messages
|
||||
codec1 := MakeType(
|
||||
"Codec1",
|
||||
Is[int](),
|
||||
func(i int) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.FailureWithMessage[int](i, "codec1 error")(c)
|
||||
}
|
||||
},
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
codec2 := MakeType(
|
||||
"Codec2",
|
||||
Is[int](),
|
||||
func(i int) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.FailureWithMessage[int](i, "codec2 error")(c)
|
||||
}
|
||||
},
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
altCodec := MonadAlt(
|
||||
codec1,
|
||||
func() Type[int, int, int] { return codec2 },
|
||||
)
|
||||
|
||||
result := altCodec.Decode(42)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(int) validation.Errors { return nil },
|
||||
)
|
||||
|
||||
require.NotNil(t, errors)
|
||||
require.GreaterOrEqual(t, len(errors), 2)
|
||||
|
||||
// Check that both error messages are present
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
|
||||
hasCodec1Error := false
|
||||
hasCodec2Error := false
|
||||
for _, msg := range messages {
|
||||
if msg == "codec1 error" {
|
||||
hasCodec1Error = true
|
||||
}
|
||||
if msg == "codec2 error" {
|
||||
hasCodec2Error = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, hasCodec1Error, "should have error from first codec")
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
package decode
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/readert"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readereither"
|
||||
)
|
||||
|
||||
// Of creates a Decode that always succeeds with the given value.
|
||||
@@ -14,7 +15,82 @@ import (
|
||||
// decoder := decode.Of[string](42)
|
||||
// result := decoder("any input") // Always returns validation.Success(42)
|
||||
func Of[I, A any](a A) Decode[I, A] {
|
||||
return reader.Of[I](validation.Of(a))
|
||||
return readereither.Of[I, Errors](a)
|
||||
}
|
||||
|
||||
// Left creates a Decode that always fails with the given validation errors.
|
||||
// This is the dual of Of - while Of lifts a success value, Left lifts failure errors
|
||||
// into the Decode context.
|
||||
//
|
||||
// Left is useful for:
|
||||
// - Creating decoders that represent known failure states
|
||||
// - Short-circuiting decode pipelines with specific errors
|
||||
// - Building custom validation error responses
|
||||
// - Testing error handling paths
|
||||
//
|
||||
// The returned decoder ignores its input and always returns a validation failure
|
||||
// containing the provided errors. This makes it the identity element for the
|
||||
// Alt/OrElse operations when used as a fallback.
|
||||
//
|
||||
// Type signature: func(Errors) Decode[I, A]
|
||||
// - Takes validation errors
|
||||
// - Returns a decoder that always fails with those errors
|
||||
// - The decoder ignores its input of type I
|
||||
// - The failure type A can be any type (phantom type)
|
||||
//
|
||||
// Example - Creating a failing decoder:
|
||||
//
|
||||
// failDecoder := decode.Left[string, int](validation.Errors{
|
||||
// &validation.ValidationError{
|
||||
// Value: nil,
|
||||
// Messsage: "operation not supported",
|
||||
// },
|
||||
// })
|
||||
// result := failDecoder("any input") // Always fails with the error
|
||||
//
|
||||
// Example - Short-circuiting with specific errors:
|
||||
//
|
||||
// validateAge := func(age int) Decode[map[string]any, int] {
|
||||
// if age < 0 {
|
||||
// return decode.Left[map[string]any, int](validation.Errors{
|
||||
// &validation.ValidationError{
|
||||
// Value: age,
|
||||
// Context: validation.Context{{Key: "age", Type: "int"}},
|
||||
// Messsage: "age cannot be negative",
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
// return decode.Of[map[string]any](age)
|
||||
// }
|
||||
//
|
||||
// Example - Building error responses:
|
||||
//
|
||||
// notFoundError := decode.Left[string, User](validation.Errors{
|
||||
// &validation.ValidationError{
|
||||
// Messsage: "user not found",
|
||||
// },
|
||||
// })
|
||||
//
|
||||
// decoder := decode.MonadAlt(
|
||||
// tryFindUser,
|
||||
// func() Decode[string, User] { return notFoundError },
|
||||
// )
|
||||
//
|
||||
// Example - Testing error paths:
|
||||
//
|
||||
// // Create a decoder that always fails for testing
|
||||
// alwaysFails := decode.Left[string, int](validation.Errors{
|
||||
// &validation.ValidationError{Messsage: "test error"},
|
||||
// })
|
||||
//
|
||||
// // Test error recovery logic
|
||||
// recovered := decode.OrElse(func(errs Errors) Decode[string, int] {
|
||||
// return decode.Of[string](0) // recover with default
|
||||
// })(alwaysFails)
|
||||
//
|
||||
// result := recovered("input") // Success(0)
|
||||
func Left[I, A any](err Errors) Decode[I, A] {
|
||||
return readereither.Left[I, A](err)
|
||||
}
|
||||
|
||||
// MonadChain sequences two decode operations, passing the result of the first to the second.
|
||||
@@ -50,6 +126,60 @@ func Chain[I, A, B any](f Kleisli[I, A, B]) Operator[I, A, B] {
|
||||
)
|
||||
}
|
||||
|
||||
// ChainLeft transforms the error channel of a decoder, enabling error recovery and context addition.
|
||||
// This is the left-biased monadic chain operation that operates on validation failures.
|
||||
//
|
||||
// **Key behaviors**:
|
||||
// - Success values pass through unchanged - the handler is never called
|
||||
// - On failure, the handler receives the errors and can recover or add context
|
||||
// - When the handler also fails, **both original and new errors are aggregated**
|
||||
// - The handler returns a Decode[I, A], giving it access to the original input
|
||||
//
|
||||
// **Error Aggregation**: Unlike standard Either operations, when the transformation function
|
||||
// returns a failure, both the original errors AND the new errors are combined using the
|
||||
// Errors monoid. This ensures no validation errors are lost.
|
||||
//
|
||||
// Use cases:
|
||||
// - Adding contextual information to validation errors
|
||||
// - Recovering from specific error conditions
|
||||
// - Transforming error messages while preserving original errors
|
||||
// - Implementing conditional recovery based on error types
|
||||
//
|
||||
// Example - Error recovery:
|
||||
//
|
||||
// failingDecoder := func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Value: input, Messsage: "not found"},
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// recoverFromNotFound := ChainLeft(func(errs Errors) Decode[string, int] {
|
||||
// for _, err := range errs {
|
||||
// if err.Messsage == "not found" {
|
||||
// return Of[string](0) // recover with default
|
||||
// }
|
||||
// }
|
||||
// return func(input string) Validation[int] {
|
||||
// return either.Left[int](errs)
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// decoder := recoverFromNotFound(failingDecoder)
|
||||
// result := decoder("input") // Success(0) - recovered from failure
|
||||
//
|
||||
// Example - Adding context:
|
||||
//
|
||||
// addContext := ChainLeft(func(errs Errors) Decode[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {
|
||||
// Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
|
||||
// Messsage: "failed to decode user age",
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
// // Result will contain BOTH original error and context error
|
||||
func ChainLeft[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
|
||||
return readert.Chain[Decode[I, A]](
|
||||
validation.ChainLeft,
|
||||
@@ -57,6 +187,147 @@ func ChainLeft[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
|
||||
)
|
||||
}
|
||||
|
||||
// MonadChainLeft transforms the error channel of a decoder, enabling error recovery and context addition.
|
||||
// This is the uncurried version of ChainLeft, taking both the decoder and the transformation function directly.
|
||||
//
|
||||
// **Key behaviors**:
|
||||
// - Success values pass through unchanged - the handler is never called
|
||||
// - On failure, the handler receives the errors and can recover or add context
|
||||
// - When the handler also fails, **both original and new errors are aggregated**
|
||||
// - The handler returns a Decode[I, A], giving it access to the original input
|
||||
//
|
||||
// **Error Aggregation**: Unlike standard Either operations, when the transformation function
|
||||
// returns a failure, both the original errors AND the new errors are combined using the
|
||||
// Errors monoid. This ensures no validation errors are lost.
|
||||
//
|
||||
// This function is the direct, uncurried form of ChainLeft. Use ChainLeft when you need
|
||||
// a curried operator for composition pipelines, and use MonadChainLeft when you have both
|
||||
// the decoder and transformation function available at once.
|
||||
//
|
||||
// Use cases:
|
||||
// - Adding contextual information to validation errors
|
||||
// - Recovering from specific error conditions
|
||||
// - Transforming error messages while preserving original errors
|
||||
// - Implementing conditional recovery based on error types
|
||||
//
|
||||
// Example - Error recovery:
|
||||
//
|
||||
// failingDecoder := func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Value: input, Messsage: "not found"},
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// recoverFromNotFound := func(errs Errors) Decode[string, int] {
|
||||
// for _, err := range errs {
|
||||
// if err.Messsage == "not found" {
|
||||
// return Of[string](0) // recover with default
|
||||
// }
|
||||
// }
|
||||
// return func(input string) Validation[int] {
|
||||
// return either.Left[int](errs)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// decoder := MonadChainLeft(failingDecoder, recoverFromNotFound)
|
||||
// result := decoder("input") // Success(0) - recovered from failure
|
||||
//
|
||||
// Example - Adding context:
|
||||
//
|
||||
// addContext := func(errs Errors) Decode[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {
|
||||
// Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
|
||||
// Messsage: "failed to decode user age",
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// decoder := MonadChainLeft(failingDecoder, addContext)
|
||||
// result := decoder("abc")
|
||||
// // Result will contain BOTH original error and context error
|
||||
//
|
||||
// Example - Comparison with ChainLeft:
|
||||
//
|
||||
// // MonadChainLeft - direct application
|
||||
// result1 := MonadChainLeft(decoder, handler)("input")
|
||||
//
|
||||
// // ChainLeft - curried for pipelines
|
||||
// result2 := ChainLeft(handler)(decoder)("input")
|
||||
//
|
||||
// // Both produce identical results
|
||||
func MonadChainLeft[I, A any](fa Decode[I, A], f Kleisli[I, Errors, A]) Decode[I, A] {
|
||||
return readert.MonadChain(
|
||||
validation.MonadChainLeft,
|
||||
fa,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// OrElse provides fallback decoding logic when the primary decoder fails.
|
||||
// This is an alias for ChainLeft with a more semantic name for fallback scenarios.
|
||||
//
|
||||
// **OrElse is exactly the same as ChainLeft** - they are aliases with identical implementations
|
||||
// and behavior. The choice between them is purely about code readability and semantic intent:
|
||||
// - Use **OrElse** when emphasizing fallback/alternative decoding logic
|
||||
// - Use **ChainLeft** when emphasizing technical error channel transformation
|
||||
//
|
||||
// **Key behaviors** (identical to ChainLeft):
|
||||
// - Success values pass through unchanged - the handler is never called
|
||||
// - On failure, the handler receives the errors and can provide an alternative
|
||||
// - When the handler also fails, **both original and new errors are aggregated**
|
||||
// - The handler returns a Decode[I, A], giving it access to the original input
|
||||
//
|
||||
// The name "OrElse" reads naturally in code: "try this decoder, or else try this alternative."
|
||||
// This makes it ideal for expressing fallback logic and default values.
|
||||
//
|
||||
// Use cases:
|
||||
// - Providing default values when decoding fails
|
||||
// - Trying alternative decoding strategies
|
||||
// - Implementing fallback chains with multiple alternatives
|
||||
// - Input-dependent recovery (using access to original input)
|
||||
//
|
||||
// Example - Simple fallback:
|
||||
//
|
||||
// primaryDecoder := func(input string) Validation[int] {
|
||||
// n, err := strconv.Atoi(input)
|
||||
// if err != nil {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Value: input, Messsage: "not a valid integer"},
|
||||
// })
|
||||
// }
|
||||
// return validation.Of(n)
|
||||
// }
|
||||
//
|
||||
// withDefault := OrElse(func(errs Errors) Decode[string, int] {
|
||||
// return Of[string](0) // default to 0 if decoding fails
|
||||
// })
|
||||
//
|
||||
// decoder := withDefault(primaryDecoder)
|
||||
// result1 := decoder("42") // Success(42)
|
||||
// result2 := decoder("abc") // Success(0) - fallback
|
||||
//
|
||||
// Example - Input-dependent fallback:
|
||||
//
|
||||
// smartDefault := OrElse(func(errs Errors) Decode[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// // Access original input to determine appropriate default
|
||||
// if strings.Contains(input, "http") {
|
||||
// return validation.Of(80)
|
||||
// }
|
||||
// if strings.Contains(input, "https") {
|
||||
// return validation.Of(443)
|
||||
// }
|
||||
// return validation.Of(8080)
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// decoder := smartDefault(decodePort)
|
||||
// result1 := decoder("http-server") // Success(80)
|
||||
// result2 := decoder("https-server") // Success(443)
|
||||
// result3 := decoder("other") // Success(8080)
|
||||
func OrElse[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
|
||||
return ChainLeft(f)
|
||||
}
|
||||
@@ -138,3 +409,155 @@ func Ap[B, I, A any](fa Decode[I, A]) Operator[I, func(A) B, B] {
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadAlt provides alternative/fallback decoding with error aggregation.
|
||||
// This is the Alternative pattern's core operation that tries the first decoder,
|
||||
// and if it fails, tries the second decoder as a fallback.
|
||||
//
|
||||
// **Key behaviors**:
|
||||
// - If first succeeds: returns the first result (second is never evaluated)
|
||||
// - If first fails and second succeeds: returns the second result
|
||||
// - If both fail: **aggregates errors from both decoders**
|
||||
//
|
||||
// **Error Aggregation**: Unlike simple fallback patterns, when both decoders fail,
|
||||
// MonadAlt combines ALL errors from both attempts using the Errors monoid. This ensures
|
||||
// complete visibility into why all alternatives failed, which is crucial for debugging
|
||||
// and providing comprehensive error messages to users.
|
||||
//
|
||||
// The name "Alt" comes from the Alternative type class in functional programming,
|
||||
// which represents computations with a notion of choice and failure.
|
||||
//
|
||||
// Use cases:
|
||||
// - Trying multiple decoding strategies for the same input
|
||||
// - Providing fallback decoders when primary decoder fails
|
||||
// - Building validation pipelines with multiple alternatives
|
||||
// - Implementing "try this, or else try that" logic
|
||||
//
|
||||
// Example - Simple fallback:
|
||||
//
|
||||
// primaryDecoder := func(input string) Validation[int] {
|
||||
// n, err := strconv.Atoi(input)
|
||||
// if err != nil {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Value: input, Messsage: "not a valid integer"},
|
||||
// })
|
||||
// }
|
||||
// return validation.Of(n)
|
||||
// }
|
||||
//
|
||||
// fallbackDecoder := func() Decode[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// // Try parsing as float and converting to int
|
||||
// f, err := strconv.ParseFloat(input, 64)
|
||||
// if err != nil {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Value: input, Messsage: "not a valid number"},
|
||||
// })
|
||||
// }
|
||||
// return validation.Of(int(f))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// decoder := MonadAlt(primaryDecoder, fallbackDecoder)
|
||||
// result1 := decoder("42") // Success(42) - primary succeeds
|
||||
// result2 := decoder("42.5") // Success(42) - fallback succeeds
|
||||
// result3 := decoder("abc") // Failures with both errors aggregated
|
||||
//
|
||||
// Example - Multiple alternatives:
|
||||
//
|
||||
// decoder1 := parseAsJSON
|
||||
// decoder2 := func() Decode[string, Config] { return parseAsYAML }
|
||||
// decoder3 := func() Decode[string, Config] { return parseAsINI }
|
||||
//
|
||||
// // Try JSON, then YAML, then INI
|
||||
// decoder := MonadAlt(MonadAlt(decoder1, decoder2), decoder3)
|
||||
// // If all fail, errors from all three attempts are aggregated
|
||||
//
|
||||
// Example - Error aggregation:
|
||||
//
|
||||
// failing1 := func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Messsage: "primary decoder failed"},
|
||||
// })
|
||||
// }
|
||||
// failing2 := func() Decode[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Messsage: "fallback decoder failed"},
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// decoder := MonadAlt(failing1, failing2)
|
||||
// result := decoder("input")
|
||||
// // Result contains BOTH errors: ["primary decoder failed", "fallback decoder failed"]
|
||||
func MonadAlt[I, A any](first Decode[I, A], second Lazy[Decode[I, A]]) Decode[I, A] {
|
||||
return MonadChainLeft(first, function.Ignore1of1[Errors](second))
|
||||
}
|
||||
|
||||
// Alt creates an operator that provides alternative/fallback decoding with error aggregation.
|
||||
// This is the curried version of MonadAlt, useful for composition pipelines.
|
||||
//
|
||||
// **Key behaviors** (identical to MonadAlt):
|
||||
// - If first succeeds: returns the first result (second is never evaluated)
|
||||
// - If first fails and second succeeds: returns the second result
|
||||
// - If both fail: **aggregates errors from both decoders**
|
||||
//
|
||||
// The Alt operator enables building reusable fallback chains that can be applied
|
||||
// to different decoders. It reads naturally in pipelines: "apply this decoder,
|
||||
// with this alternative if it fails."
|
||||
//
|
||||
// Use cases:
|
||||
// - Creating reusable fallback strategies
|
||||
// - Building decoder combinators with alternatives
|
||||
// - Composing multiple fallback layers
|
||||
// - Implementing retry logic with different strategies
|
||||
//
|
||||
// Example - Creating a reusable fallback:
|
||||
//
|
||||
// // Create an operator that falls back to a default value
|
||||
// withDefault := Alt(func() Decode[string, int] {
|
||||
// return Of[string](0)
|
||||
// })
|
||||
//
|
||||
// // Apply to any decoder
|
||||
// decoder1 := withDefault(parseInteger)
|
||||
// decoder2 := withDefault(parseFromJSON)
|
||||
//
|
||||
// result1 := decoder1("42") // Success(42)
|
||||
// result2 := decoder1("abc") // Success(0) - fallback
|
||||
//
|
||||
// Example - Composing multiple alternatives:
|
||||
//
|
||||
// tryYAML := Alt(func() Decode[string, Config] { return parseAsYAML })
|
||||
// tryINI := Alt(func() Decode[string, Config] { return parseAsINI })
|
||||
// useDefault := Alt(func() Decode[string, Config] {
|
||||
// return Of[string](defaultConfig)
|
||||
// })
|
||||
//
|
||||
// // Build a pipeline: try JSON, then YAML, then INI, then default
|
||||
// decoder := useDefault(tryINI(tryYAML(parseAsJSON)))
|
||||
//
|
||||
// Example - Error aggregation in pipeline:
|
||||
//
|
||||
// failing1 := func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{{Messsage: "error 1"}})
|
||||
// }
|
||||
// failing2 := func() Decode[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{{Messsage: "error 2"}})
|
||||
// }
|
||||
// }
|
||||
// failing3 := func() Decode[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{{Messsage: "error 3"}})
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Chain multiple alternatives
|
||||
// decoder := Alt(failing3)(Alt(failing2)(failing1))
|
||||
// result := decoder("input")
|
||||
// // Result contains ALL errors: ["error 1", "error 2", "error 3"]
|
||||
func Alt[I, A any](second Lazy[Decode[I, A]]) Operator[I, A, A] {
|
||||
return ChainLeft(function.Ignore1of1[Errors](second))
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
368
v2/optics/codec/decode/monoid.go
Normal file
368
v2/optics/codec/decode/monoid.go
Normal file
@@ -0,0 +1,368 @@
|
||||
package decode
|
||||
|
||||
import "github.com/IBM/fp-go/v2/monoid"
|
||||
|
||||
// ApplicativeMonoid creates a Monoid instance for Decode[I, A] given a Monoid for A.
|
||||
// This allows combining decoders where both the decoded values and validation errors
|
||||
// are combined according to their respective monoid operations.
|
||||
//
|
||||
// The resulting monoid enables:
|
||||
// - Combining multiple decoders that produce monoidal values
|
||||
// - Accumulating validation errors when any decoder fails
|
||||
// - Building complex decoders from simpler ones through composition
|
||||
//
|
||||
// **Behavior**:
|
||||
// - Empty: Returns a decoder that always succeeds with the empty value from the inner monoid
|
||||
// - Concat: Combines two decoders:
|
||||
// - Both succeed: Combines decoded values using the inner monoid
|
||||
// - Any fails: Accumulates all validation errors using the Errors monoid
|
||||
//
|
||||
// This is particularly useful for:
|
||||
// - Aggregating results from multiple independent decoders
|
||||
// - Building decoders that combine partial results
|
||||
// - Validating and combining configuration from multiple sources
|
||||
// - Parallel validation with result accumulation
|
||||
//
|
||||
// Example - Combining string decoders:
|
||||
//
|
||||
// import S "github.com/IBM/fp-go/v2/string"
|
||||
//
|
||||
// // Create a monoid for decoders that produce strings
|
||||
// m := ApplicativeMonoid[map[string]any](S.Monoid)
|
||||
//
|
||||
// decoder1 := func(data map[string]any) Validation[string] {
|
||||
// if name, ok := data["firstName"].(string); ok {
|
||||
// return validation.Of(name)
|
||||
// }
|
||||
// return either.Left[string](validation.Errors{
|
||||
// {Messsage: "missing firstName"},
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// decoder2 := func(data map[string]any) Validation[string] {
|
||||
// if name, ok := data["lastName"].(string); ok {
|
||||
// return validation.Of(" " + name)
|
||||
// }
|
||||
// return either.Left[string](validation.Errors{
|
||||
// {Messsage: "missing lastName"},
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// // Combine decoders - will concatenate strings if both succeed
|
||||
// combined := m.Concat(decoder1, decoder2)
|
||||
// result := combined(map[string]any{
|
||||
// "firstName": "John",
|
||||
// "lastName": "Doe",
|
||||
// }) // Success("John Doe")
|
||||
//
|
||||
// Example - Error accumulation:
|
||||
//
|
||||
// // If any decoder fails, errors are accumulated
|
||||
// result := combined(map[string]any{}) // Failures with both error messages
|
||||
//
|
||||
// Example - Numeric aggregation:
|
||||
//
|
||||
// import N "github.com/IBM/fp-go/v2/number"
|
||||
//
|
||||
// intMonoid := monoid.MakeMonoid(N.Add[int], 0)
|
||||
// m := ApplicativeMonoid[string](intMonoid)
|
||||
//
|
||||
// decoder1 := func(input string) Validation[int] {
|
||||
// return validation.Of(10)
|
||||
// }
|
||||
// decoder2 := func(input string) Validation[int] {
|
||||
// return validation.Of(32)
|
||||
// }
|
||||
//
|
||||
// combined := m.Concat(decoder1, decoder2)
|
||||
// result := combined("input") // Success(42) - values are added
|
||||
func ApplicativeMonoid[I, A any](m Monoid[A]) Monoid[Decode[I, A]] {
|
||||
return monoid.ApplicativeMonoid(
|
||||
Of[I, A],
|
||||
MonadMap[I, A, Endomorphism[A]],
|
||||
MonadAp[A, I, A],
|
||||
m,
|
||||
)
|
||||
}
|
||||
|
||||
// AlternativeMonoid creates a Monoid instance for Decode[I, A] using the Alternative pattern.
|
||||
// This combines applicative error-accumulation behavior with alternative fallback behavior,
|
||||
// allowing you to both accumulate errors and provide fallback alternatives when combining decoders.
|
||||
//
|
||||
// The Alternative pattern provides two key operations:
|
||||
// - Applicative operations (Of, Map, Ap): accumulate errors when combining decoders
|
||||
// - Alternative operation (Alt): provide fallback when a decoder fails
|
||||
//
|
||||
// This monoid is particularly useful when you want to:
|
||||
// - Try multiple decoding strategies and fall back to alternatives
|
||||
// - Combine successful values using the provided monoid
|
||||
// - Accumulate all errors from failed attempts
|
||||
// - Build decoding pipelines with fallback logic
|
||||
//
|
||||
// **Behavior**:
|
||||
// - Empty: Returns a decoder that always succeeds with the empty value from the inner monoid
|
||||
// - Concat: Combines two decoders using both applicative and alternative semantics:
|
||||
// - If first succeeds and second succeeds: combines decoded values using inner monoid
|
||||
// - If first fails: tries second as fallback (alternative behavior)
|
||||
// - If both fail: **accumulates all errors from both decoders**
|
||||
//
|
||||
// **Error Aggregation**: When both decoders fail, all validation errors from both attempts
|
||||
// are combined using the Errors monoid. This provides complete visibility into why all
|
||||
// alternatives failed, which is essential for debugging and user feedback.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - I: The input type being decoded
|
||||
// - A: The output type after successful decoding
|
||||
//
|
||||
// Parameters:
|
||||
// - m: The monoid for combining successful decoded values of type A
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// A Monoid[Decode[I, A]] that combines applicative and alternative behaviors
|
||||
//
|
||||
// Example - Combining successful decoders:
|
||||
//
|
||||
// import S "github.com/IBM/fp-go/v2/string"
|
||||
//
|
||||
// m := AlternativeMonoid[string](S.Monoid)
|
||||
//
|
||||
// decoder1 := func(input string) Validation[string] {
|
||||
// return validation.Of("Hello")
|
||||
// }
|
||||
// decoder2 := func(input string) Validation[string] {
|
||||
// return validation.Of(" World")
|
||||
// }
|
||||
//
|
||||
// combined := m.Concat(decoder1, decoder2)
|
||||
// result := combined("input")
|
||||
// // Result: Success("Hello World") - values combined using string monoid
|
||||
//
|
||||
// Example - Fallback behavior:
|
||||
//
|
||||
// m := AlternativeMonoid[string](S.Monoid)
|
||||
//
|
||||
// failing := func(input string) Validation[string] {
|
||||
// return either.Left[string](validation.Errors{
|
||||
// {Value: input, Messsage: "primary failed"},
|
||||
// })
|
||||
// }
|
||||
// fallback := func(input string) Validation[string] {
|
||||
// return validation.Of("fallback value")
|
||||
// }
|
||||
//
|
||||
// combined := m.Concat(failing, fallback)
|
||||
// result := combined("input")
|
||||
// // Result: Success("fallback value") - second decoder used as fallback
|
||||
//
|
||||
// Example - Error accumulation when both fail:
|
||||
//
|
||||
// m := AlternativeMonoid[string](S.Monoid)
|
||||
//
|
||||
// failing1 := func(input string) Validation[string] {
|
||||
// return either.Left[string](validation.Errors{
|
||||
// {Value: input, Messsage: "error 1"},
|
||||
// })
|
||||
// }
|
||||
// failing2 := func(input string) Validation[string] {
|
||||
// return either.Left[string](validation.Errors{
|
||||
// {Value: input, Messsage: "error 2"},
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// combined := m.Concat(failing1, failing2)
|
||||
// result := combined("input")
|
||||
// // Result: Failures with accumulated errors: ["error 1", "error 2"]
|
||||
//
|
||||
// Example - Building decoder with multiple fallbacks:
|
||||
//
|
||||
// import N "github.com/IBM/fp-go/v2/number"
|
||||
//
|
||||
// m := AlternativeMonoid[string](N.MonoidSum[int]())
|
||||
//
|
||||
// // Try to parse from different formats
|
||||
// parseJSON := func(input string) Validation[int] { /* ... */ }
|
||||
// parseYAML := func(input string) Validation[int] { /* ... */ }
|
||||
// parseINI := func(input string) Validation[int] { /* ... */ }
|
||||
//
|
||||
// // Combine with fallback chain
|
||||
// decoder := m.Concat(m.Concat(parseJSON, parseYAML), parseINI)
|
||||
// // Uses first successful parser, or accumulates all errors if all fail
|
||||
//
|
||||
// Example - Combining multiple configuration sources:
|
||||
//
|
||||
// type Config struct{ Port int }
|
||||
// configMonoid := monoid.MakeMonoid(
|
||||
// func(a, b Config) Config {
|
||||
// if b.Port != 0 { return b }
|
||||
// return a
|
||||
// },
|
||||
// Config{Port: 0},
|
||||
// )
|
||||
//
|
||||
// m := AlternativeMonoid[map[string]any](configMonoid)
|
||||
//
|
||||
// fromEnv := func(data map[string]any) Validation[Config] { /* ... */ }
|
||||
// fromFile := func(data map[string]any) Validation[Config] { /* ... */ }
|
||||
// fromDefault := func(data map[string]any) Validation[Config] {
|
||||
// return validation.Of(Config{Port: 8080})
|
||||
// }
|
||||
//
|
||||
// // Try env, then file, then default
|
||||
// decoder := m.Concat(m.Concat(fromEnv, fromFile), fromDefault)
|
||||
// // Returns first successful config, or all errors if all fail
|
||||
func AlternativeMonoid[I, A any](m Monoid[A]) Monoid[Decode[I, A]] {
|
||||
return monoid.AlternativeMonoid(
|
||||
Of[I, A],
|
||||
MonadMap[I, A, func(A) A],
|
||||
MonadAp[A, I, A],
|
||||
MonadAlt[I, A],
|
||||
m,
|
||||
)
|
||||
}
|
||||
|
||||
// AltMonoid creates a Monoid instance for Decode[I, A] using the Alt (alternative) operation.
|
||||
// This monoid provides a way to combine decoders with fallback behavior, where the second
|
||||
// decoder is used as an alternative if the first one fails.
|
||||
//
|
||||
// The Alt operation implements the "try first, fallback to second" pattern, which is useful
|
||||
// for decoding scenarios where you want to attempt multiple decoding strategies in sequence
|
||||
// and use the first one that succeeds.
|
||||
//
|
||||
// **Behavior**:
|
||||
// - Empty: Returns the provided zero value (a lazy computation that produces a Decode[I, A])
|
||||
// - Concat: Combines two decoders using Alt semantics:
|
||||
// - If first succeeds: returns the first result (second is never evaluated)
|
||||
// - If first fails: tries the second decoder as fallback
|
||||
// - If both fail: **aggregates errors from both decoders**
|
||||
//
|
||||
// **Error Aggregation**: When both decoders fail, all validation errors from both attempts
|
||||
// are combined using the Errors monoid. This ensures complete visibility into why all
|
||||
// alternatives failed.
|
||||
//
|
||||
// This is different from [AlternativeMonoid] in that:
|
||||
// - AltMonoid uses a custom zero value (provided by the user)
|
||||
// - AlternativeMonoid derives the zero from an inner monoid
|
||||
// - AltMonoid is simpler and only provides fallback behavior
|
||||
// - AlternativeMonoid combines applicative and alternative behaviors
|
||||
//
|
||||
// Type Parameters:
|
||||
// - I: The input type being decoded
|
||||
// - A: The output type after successful decoding
|
||||
//
|
||||
// Parameters:
|
||||
// - zero: A lazy computation that produces the identity/empty Decode[I, A].
|
||||
// This is typically a decoder that always succeeds with a default value, or could be
|
||||
// a decoder that always fails representing "no decoding attempted"
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// A Monoid[Decode[I, A]] that combines decoders with fallback behavior
|
||||
//
|
||||
// Example - Using default value as zero:
|
||||
//
|
||||
// m := AltMonoid(func() Decode[string, int] {
|
||||
// return Of[string](0)
|
||||
// })
|
||||
//
|
||||
// failing := func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Value: input, Messsage: "failed"},
|
||||
// })
|
||||
// }
|
||||
// succeeding := func(input string) Validation[int] {
|
||||
// return validation.Of(42)
|
||||
// }
|
||||
//
|
||||
// combined := m.Concat(failing, succeeding)
|
||||
// result := combined("input")
|
||||
// // Result: Success(42) - falls back to second decoder
|
||||
//
|
||||
// empty := m.Empty()
|
||||
// result2 := empty("input")
|
||||
// // Result: Success(0) - the provided zero value
|
||||
//
|
||||
// Example - Chaining multiple fallbacks:
|
||||
//
|
||||
// m := AltMonoid(func() Decode[string, Config] {
|
||||
// return Of[string](defaultConfig)
|
||||
// })
|
||||
//
|
||||
// primary := parseFromPrimarySource // Fails
|
||||
// secondary := parseFromSecondarySource // Fails
|
||||
// tertiary := parseFromTertiarySource // Succeeds
|
||||
//
|
||||
// // Chain fallbacks
|
||||
// decoder := m.Concat(m.Concat(primary, secondary), tertiary)
|
||||
// result := decoder("input")
|
||||
// // Result: Success from tertiary - uses first successful decoder
|
||||
//
|
||||
// Example - Error aggregation when all fail:
|
||||
//
|
||||
// m := AltMonoid(func() Decode[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Messsage: "no default available"},
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// failing1 := func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Value: input, Messsage: "error 1"},
|
||||
// })
|
||||
// }
|
||||
// failing2 := func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Value: input, Messsage: "error 2"},
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// combined := m.Concat(failing1, failing2)
|
||||
// result := combined("input")
|
||||
// // Result: Failures with accumulated errors: ["error 1", "error 2"]
|
||||
//
|
||||
// Example - Building a decoder pipeline with fallbacks:
|
||||
//
|
||||
// m := AltMonoid(func() Decode[string, Config] {
|
||||
// return Of[string](defaultConfig)
|
||||
// })
|
||||
//
|
||||
// // Try multiple decoding sources in order
|
||||
// decoders := []Decode[string, Config]{
|
||||
// loadFromFile("config.json"), // Try file first
|
||||
// loadFromEnv, // Then environment
|
||||
// loadFromRemote("api.example.com"), // Then remote API
|
||||
// }
|
||||
//
|
||||
// // Fold using the monoid to get first successful config
|
||||
// result := array.MonoidFold(m)(decoders)
|
||||
// // Result: First successful config, or defaultConfig if all fail
|
||||
//
|
||||
// Example - Comparing with AlternativeMonoid:
|
||||
//
|
||||
// // AltMonoid - simple fallback with custom zero
|
||||
// altM := AltMonoid(func() Decode[string, int] {
|
||||
// return Of[string](0)
|
||||
// })
|
||||
//
|
||||
// // AlternativeMonoid - combines values when both succeed
|
||||
// import N "github.com/IBM/fp-go/v2/number"
|
||||
// altMonoid := AlternativeMonoid[string](N.MonoidSum[int]())
|
||||
//
|
||||
// decoder1 := Of[string](10)
|
||||
// decoder2 := Of[string](32)
|
||||
//
|
||||
// // AltMonoid: returns first success (10)
|
||||
// result1 := altM.Concat(decoder1, decoder2)("input")
|
||||
// // Result: Success(10)
|
||||
//
|
||||
// // AlternativeMonoid: combines both successes (10 + 32 = 42)
|
||||
// result2 := altMonoid.Concat(decoder1, decoder2)("input")
|
||||
// // Result: Success(42)
|
||||
func AltMonoid[I, A any](zero Lazy[Decode[I, A]]) Monoid[Decode[I, A]] {
|
||||
return monoid.AltMonoid(
|
||||
zero,
|
||||
MonadAlt[I, A],
|
||||
)
|
||||
}
|
||||
970
v2/optics/codec/decode/monoid_test.go
Normal file
970
v2/optics/codec/decode/monoid_test.go
Normal file
@@ -0,0 +1,970 @@
|
||||
package decode
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
MO "github.com/IBM/fp-go/v2/monoid"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestApplicativeMonoid(t *testing.T) {
|
||||
t.Run("with string monoid", func(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](S.Monoid)
|
||||
|
||||
t.Run("empty returns decoder that succeeds with empty string", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
result := empty("any input")
|
||||
|
||||
assert.Equal(t, validation.Of(""), result)
|
||||
})
|
||||
|
||||
t.Run("concat combines successful decoders", func(t *testing.T) {
|
||||
decoder1 := Of[string]("Hello")
|
||||
decoder2 := Of[string](" World")
|
||||
|
||||
combined := m.Concat(decoder1, decoder2)
|
||||
result := combined("input")
|
||||
|
||||
assert.Equal(t, validation.Of("Hello World"), result)
|
||||
})
|
||||
|
||||
t.Run("concat with failure returns failure", func(t *testing.T) {
|
||||
decoder1 := Of[string]("Hello")
|
||||
decoder2 := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "decode failed"},
|
||||
})
|
||||
}
|
||||
|
||||
combined := m.Concat(decoder1, decoder2)
|
||||
result := combined("input")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "decode failed", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("concat accumulates all errors from both failures", func(t *testing.T) {
|
||||
decoder1 := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "error 1"},
|
||||
})
|
||||
}
|
||||
decoder2 := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "error 2"},
|
||||
})
|
||||
}
|
||||
|
||||
combined := m.Concat(decoder1, decoder2)
|
||||
result := combined("input")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2)
|
||||
messages := []string{errors[0].Messsage, errors[1].Messsage}
|
||||
assert.Contains(t, messages, "error 1")
|
||||
assert.Contains(t, messages, "error 2")
|
||||
})
|
||||
|
||||
t.Run("concat with empty preserves decoder", func(t *testing.T) {
|
||||
decoder := Of[string]("test")
|
||||
empty := m.Empty()
|
||||
|
||||
result1 := m.Concat(decoder, empty)("input")
|
||||
result2 := m.Concat(empty, decoder)("input")
|
||||
|
||||
val1 := either.MonadFold(result1,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
val2 := either.MonadFold(result2,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
assert.Equal(t, "test", val1)
|
||||
assert.Equal(t, "test", val2)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with int addition monoid", func(t *testing.T) {
|
||||
intMonoid := MO.MakeMonoid(
|
||||
func(a, b int) int { return a + b },
|
||||
0,
|
||||
)
|
||||
m := ApplicativeMonoid[string](intMonoid)
|
||||
|
||||
t.Run("empty returns decoder with zero", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
result := empty("input")
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return -1 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 0, value)
|
||||
})
|
||||
|
||||
t.Run("concat adds decoded values", func(t *testing.T) {
|
||||
decoder1 := Of[string](10)
|
||||
decoder2 := Of[string](32)
|
||||
|
||||
combined := m.Concat(decoder1, decoder2)
|
||||
result := combined("input")
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("multiple concat operations", func(t *testing.T) {
|
||||
decoder1 := Of[string](1)
|
||||
decoder2 := Of[string](2)
|
||||
decoder3 := Of[string](3)
|
||||
decoder4 := Of[string](4)
|
||||
|
||||
combined := m.Concat(m.Concat(m.Concat(decoder1, decoder2), decoder3), decoder4)
|
||||
result := combined("input")
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 10, value)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with map input type", func(t *testing.T) {
|
||||
m := ApplicativeMonoid[map[string]any](S.Monoid)
|
||||
|
||||
t.Run("combines decoders with different inputs", func(t *testing.T) {
|
||||
decoder1 := func(data map[string]any) Validation[string] {
|
||||
if name, ok := data["firstName"].(string); ok {
|
||||
return validation.Of(name)
|
||||
}
|
||||
return either.Left[string](validation.Errors{
|
||||
{Messsage: "missing firstName"},
|
||||
})
|
||||
}
|
||||
|
||||
decoder2 := func(data map[string]any) Validation[string] {
|
||||
if name, ok := data["lastName"].(string); ok {
|
||||
return validation.Of(" " + name)
|
||||
}
|
||||
return either.Left[string](validation.Errors{
|
||||
{Messsage: "missing lastName"},
|
||||
})
|
||||
}
|
||||
|
||||
combined := m.Concat(decoder1, decoder2)
|
||||
|
||||
// Test success case
|
||||
result1 := combined(map[string]any{
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
})
|
||||
value1 := either.MonadFold(result1,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "John Doe", value1)
|
||||
|
||||
// Test failure case - both fields missing
|
||||
result2 := combined(map[string]any{})
|
||||
assert.True(t, either.IsLeft(result2))
|
||||
errors := either.MonadFold(result2,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonoidLaws(t *testing.T) {
|
||||
t.Run("ApplicativeMonoid satisfies monoid laws", func(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](S.Monoid)
|
||||
|
||||
decoder1 := Of[string]("a")
|
||||
decoder2 := Of[string]("b")
|
||||
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
// empty + a = a
|
||||
result := m.Concat(m.Empty(), decoder1)("input")
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "a", value)
|
||||
})
|
||||
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
// a + empty = a
|
||||
result := m.Concat(decoder1, m.Empty())("input")
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "a", value)
|
||||
})
|
||||
|
||||
t.Run("associativity", func(t *testing.T) {
|
||||
decoder3 := Of[string]("c")
|
||||
// (a + b) + c = a + (b + c)
|
||||
left := m.Concat(m.Concat(decoder1, decoder2), decoder3)("input")
|
||||
right := m.Concat(decoder1, m.Concat(decoder2, decoder3))("input")
|
||||
|
||||
leftVal := either.MonadFold(left,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
rightVal := either.MonadFold(right,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
assert.Equal(t, "abc", leftVal)
|
||||
assert.Equal(t, "abc", rightVal)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeMonoidWithFailures(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](S.Monoid)
|
||||
|
||||
t.Run("failure propagates through concat", func(t *testing.T) {
|
||||
decoder1 := Of[string]("a")
|
||||
decoder2 := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "error"},
|
||||
})
|
||||
}
|
||||
decoder3 := Of[string]("c")
|
||||
|
||||
combined := m.Concat(m.Concat(decoder1, decoder2), decoder3)
|
||||
result := combined("input")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
})
|
||||
|
||||
t.Run("multiple failures accumulate", func(t *testing.T) {
|
||||
decoder1 := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "error 1"},
|
||||
})
|
||||
}
|
||||
decoder2 := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "error 2"},
|
||||
})
|
||||
}
|
||||
decoder3 := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "error 3"},
|
||||
})
|
||||
}
|
||||
|
||||
combined := m.Concat(m.Concat(decoder1, decoder2), decoder3)
|
||||
result := combined("input")
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 3)
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "error 1")
|
||||
assert.Contains(t, messages, "error 2")
|
||||
assert.Contains(t, messages, "error 3")
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeMonoidEdgeCases(t *testing.T) {
|
||||
t.Run("with custom struct monoid", func(t *testing.T) {
|
||||
type Counter struct{ Count int }
|
||||
|
||||
counterMonoid := MO.MakeMonoid(
|
||||
func(a, b Counter) Counter { return Counter{Count: a.Count + b.Count} },
|
||||
Counter{Count: 0},
|
||||
)
|
||||
|
||||
m := ApplicativeMonoid[string](counterMonoid)
|
||||
|
||||
decoder1 := Of[string](Counter{Count: 5})
|
||||
decoder2 := Of[string](Counter{Count: 10})
|
||||
|
||||
combined := m.Concat(decoder1, decoder2)
|
||||
result := combined("input")
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) Counter { return Counter{} },
|
||||
F.Identity[Counter],
|
||||
)
|
||||
assert.Equal(t, 15, value.Count)
|
||||
})
|
||||
|
||||
t.Run("empty concat empty", func(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](S.Monoid)
|
||||
|
||||
combined := m.Concat(m.Empty(), m.Empty())
|
||||
result := combined("input")
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "ERROR" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "", value)
|
||||
})
|
||||
|
||||
t.Run("with different input types", func(t *testing.T) {
|
||||
intMonoid := MO.MakeMonoid(
|
||||
func(a, b int) int { return a + b },
|
||||
0,
|
||||
)
|
||||
m := ApplicativeMonoid[int](intMonoid)
|
||||
|
||||
decoder1 := func(input int) Validation[int] {
|
||||
return validation.Of(input * 2)
|
||||
}
|
||||
decoder2 := func(input int) Validation[int] {
|
||||
return validation.Of(input + 10)
|
||||
}
|
||||
|
||||
combined := m.Concat(decoder1, decoder2)
|
||||
result := combined(5)
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
// (5 * 2) + (5 + 10) = 10 + 15 = 25
|
||||
assert.Equal(t, 25, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeMonoidRealWorldScenarios(t *testing.T) {
|
||||
t.Run("combining configuration from multiple sources", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
// Monoid that combines configs (last non-empty wins for strings, sum for ints)
|
||||
configMonoid := MO.MakeMonoid(
|
||||
func(a, b Config) Config {
|
||||
host := a.Host
|
||||
if b.Host != "" {
|
||||
host = b.Host
|
||||
}
|
||||
return Config{
|
||||
Host: host,
|
||||
Port: a.Port + b.Port,
|
||||
}
|
||||
},
|
||||
Config{Host: "", Port: 0},
|
||||
)
|
||||
|
||||
m := ApplicativeMonoid[map[string]any](configMonoid)
|
||||
|
||||
decoder1 := func(data map[string]any) Validation[Config] {
|
||||
if host, ok := data["host"].(string); ok {
|
||||
return validation.Of(Config{Host: host, Port: 0})
|
||||
}
|
||||
return either.Left[Config](validation.Errors{
|
||||
{Messsage: "missing host"},
|
||||
})
|
||||
}
|
||||
|
||||
decoder2 := func(data map[string]any) Validation[Config] {
|
||||
if port, ok := data["port"].(int); ok {
|
||||
return validation.Of(Config{Host: "", Port: port})
|
||||
}
|
||||
return either.Left[Config](validation.Errors{
|
||||
{Messsage: "missing port"},
|
||||
})
|
||||
}
|
||||
|
||||
combined := m.Concat(decoder1, decoder2)
|
||||
|
||||
// Success case
|
||||
result := combined(map[string]any{
|
||||
"host": "localhost",
|
||||
"port": 8080,
|
||||
})
|
||||
|
||||
config := either.MonadFold(result,
|
||||
func(Errors) Config { return Config{} },
|
||||
F.Identity[Config],
|
||||
)
|
||||
assert.Equal(t, "localhost", config.Host)
|
||||
assert.Equal(t, 8080, config.Port)
|
||||
})
|
||||
|
||||
t.Run("aggregating validation results", func(t *testing.T) {
|
||||
intMonoid := MO.MakeMonoid(
|
||||
func(a, b int) int { return a + b },
|
||||
0,
|
||||
)
|
||||
m := ApplicativeMonoid[string](intMonoid)
|
||||
|
||||
// Decoder that extracts and validates a number
|
||||
makeDecoder := func(value int, shouldFail bool) Decode[string, int] {
|
||||
return func(input string) Validation[int] {
|
||||
if shouldFail {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "validation failed"},
|
||||
})
|
||||
}
|
||||
return validation.Of(value)
|
||||
}
|
||||
}
|
||||
|
||||
// All succeed - values are summed
|
||||
decoder1 := makeDecoder(10, false)
|
||||
decoder2 := makeDecoder(20, false)
|
||||
decoder3 := makeDecoder(12, false)
|
||||
|
||||
combined := m.Concat(m.Concat(decoder1, decoder2), decoder3)
|
||||
result := combined("input")
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
|
||||
// Some fail - errors are accumulated
|
||||
decoder4 := makeDecoder(10, true)
|
||||
decoder5 := makeDecoder(20, true)
|
||||
|
||||
combinedFail := m.Concat(decoder4, decoder5)
|
||||
resultFail := combinedFail("input")
|
||||
|
||||
assert.True(t, either.IsLeft(resultFail))
|
||||
errors := either.MonadFold(resultFail,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAlternativeMonoid tests the AlternativeMonoid function
|
||||
func TestAlternativeMonoid(t *testing.T) {
|
||||
t.Run("with string monoid", func(t *testing.T) {
|
||||
m := AlternativeMonoid[string](S.Monoid)
|
||||
|
||||
t.Run("empty returns decoder that succeeds with empty string", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
result := empty("input")
|
||||
|
||||
assert.Equal(t, validation.Of(""), result)
|
||||
})
|
||||
|
||||
t.Run("concat combines successful decoders using monoid", func(t *testing.T) {
|
||||
decoder1 := Of[string]("Hello")
|
||||
decoder2 := Of[string](" World")
|
||||
|
||||
combined := m.Concat(decoder1, decoder2)
|
||||
result := combined("input")
|
||||
|
||||
assert.Equal(t, validation.Of("Hello World"), result)
|
||||
})
|
||||
|
||||
t.Run("concat uses second as fallback when first fails", func(t *testing.T) {
|
||||
failing := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "first failed"},
|
||||
})
|
||||
}
|
||||
succeeding := Of[string]("fallback")
|
||||
|
||||
combined := m.Concat(failing, succeeding)
|
||||
result := combined("input")
|
||||
|
||||
assert.Equal(t, validation.Of("fallback"), result)
|
||||
})
|
||||
|
||||
t.Run("concat aggregates errors when both fail", func(t *testing.T) {
|
||||
failing1 := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "error 1"},
|
||||
})
|
||||
}
|
||||
failing2 := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "error 2"},
|
||||
})
|
||||
}
|
||||
|
||||
combined := m.Concat(failing1, failing2)
|
||||
result := combined("input")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors from both decoders")
|
||||
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "error 1")
|
||||
assert.Contains(t, messages, "error 2")
|
||||
})
|
||||
|
||||
t.Run("concat with empty preserves decoder", func(t *testing.T) {
|
||||
decoder := Of[string]("test")
|
||||
empty := m.Empty()
|
||||
|
||||
result1 := m.Concat(decoder, empty)("input")
|
||||
result2 := m.Concat(empty, decoder)("input")
|
||||
|
||||
val1 := either.MonadFold(result1,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
val2 := either.MonadFold(result2,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
assert.Equal(t, "test", val1)
|
||||
assert.Equal(t, "test", val2)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with int addition monoid", func(t *testing.T) {
|
||||
intMonoid := MO.MakeMonoid(
|
||||
func(a, b int) int { return a + b },
|
||||
0,
|
||||
)
|
||||
m := AlternativeMonoid[string](intMonoid)
|
||||
|
||||
t.Run("empty returns decoder with zero", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
result := empty("input")
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return -1 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 0, value)
|
||||
})
|
||||
|
||||
t.Run("concat combines decoded values when both succeed", func(t *testing.T) {
|
||||
decoder1 := Of[string](10)
|
||||
decoder2 := Of[string](32)
|
||||
|
||||
combined := m.Concat(decoder1, decoder2)
|
||||
result := combined("input")
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("concat uses fallback when first fails", func(t *testing.T) {
|
||||
failing := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "failed"},
|
||||
})
|
||||
}
|
||||
succeeding := Of[string](42)
|
||||
|
||||
combined := m.Concat(failing, succeeding)
|
||||
result := combined("input")
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("multiple concat operations", func(t *testing.T) {
|
||||
decoder1 := Of[string](1)
|
||||
decoder2 := Of[string](2)
|
||||
decoder3 := Of[string](3)
|
||||
decoder4 := Of[string](4)
|
||||
|
||||
combined := m.Concat(m.Concat(m.Concat(decoder1, decoder2), decoder3), decoder4)
|
||||
result := combined("input")
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 10, value)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("satisfies monoid laws", func(t *testing.T) {
|
||||
m := AlternativeMonoid[string](S.Monoid)
|
||||
|
||||
decoder1 := Of[string]("a")
|
||||
decoder2 := Of[string]("b")
|
||||
decoder3 := Of[string]("c")
|
||||
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
result := m.Concat(m.Empty(), decoder1)("input")
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "a", value)
|
||||
})
|
||||
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
result := m.Concat(decoder1, m.Empty())("input")
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "a", value)
|
||||
})
|
||||
|
||||
t.Run("associativity", func(t *testing.T) {
|
||||
left := m.Concat(m.Concat(decoder1, decoder2), decoder3)("input")
|
||||
right := m.Concat(decoder1, m.Concat(decoder2, decoder3))("input")
|
||||
|
||||
leftVal := either.MonadFold(left,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
rightVal := either.MonadFold(right,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
assert.Equal(t, "abc", leftVal)
|
||||
assert.Equal(t, "abc", rightVal)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("error aggregation with multiple failures", func(t *testing.T) {
|
||||
m := AlternativeMonoid[string](S.Monoid)
|
||||
|
||||
failing1 := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "error 1"},
|
||||
})
|
||||
}
|
||||
failing2 := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "error 2"},
|
||||
})
|
||||
}
|
||||
failing3 := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "error 3"},
|
||||
})
|
||||
}
|
||||
|
||||
combined := m.Concat(m.Concat(failing1, failing2), failing3)
|
||||
result := combined("input")
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
assert.GreaterOrEqual(t, len(errors), 3, "Should aggregate errors from all decoders")
|
||||
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "error 1")
|
||||
assert.Contains(t, messages, "error 2")
|
||||
assert.Contains(t, messages, "error 3")
|
||||
})
|
||||
}
|
||||
|
||||
// TestAltMonoid tests the AltMonoid function
|
||||
func TestAltMonoid(t *testing.T) {
|
||||
t.Run("with default value as zero", func(t *testing.T) {
|
||||
m := AltMonoid(func() Decode[string, int] {
|
||||
return Of[string](0)
|
||||
})
|
||||
|
||||
t.Run("empty returns the provided zero decoder", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
result := empty("input")
|
||||
|
||||
assert.Equal(t, validation.Of(0), result)
|
||||
})
|
||||
|
||||
t.Run("concat returns first decoder when it succeeds", func(t *testing.T) {
|
||||
decoder1 := Of[string](42)
|
||||
decoder2 := Of[string](100)
|
||||
|
||||
combined := m.Concat(decoder1, decoder2)
|
||||
result := combined("input")
|
||||
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("concat uses second as fallback when first fails", func(t *testing.T) {
|
||||
failing := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "failed"},
|
||||
})
|
||||
}
|
||||
succeeding := Of[string](42)
|
||||
|
||||
combined := m.Concat(failing, succeeding)
|
||||
result := combined("input")
|
||||
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("concat aggregates errors when both fail", func(t *testing.T) {
|
||||
failing1 := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 1"},
|
||||
})
|
||||
}
|
||||
failing2 := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 2"},
|
||||
})
|
||||
}
|
||||
|
||||
combined := m.Concat(failing1, failing2)
|
||||
result := combined("input")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors from both decoders")
|
||||
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "error 1")
|
||||
assert.Contains(t, messages, "error 2")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with failing zero", func(t *testing.T) {
|
||||
m := AltMonoid(func() Decode[string, int] {
|
||||
return func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Messsage: "no default available"},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty returns the failing zero decoder", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
result := empty("input")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("concat with all failures aggregates errors", func(t *testing.T) {
|
||||
failing1 := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 1"},
|
||||
})
|
||||
}
|
||||
failing2 := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 2"},
|
||||
})
|
||||
}
|
||||
|
||||
combined := m.Concat(failing1, failing2)
|
||||
result := combined("input")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("chaining multiple fallbacks", func(t *testing.T) {
|
||||
m := AltMonoid(func() Decode[string, string] {
|
||||
return Of[string]("default")
|
||||
})
|
||||
|
||||
primary := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "primary failed"},
|
||||
})
|
||||
}
|
||||
secondary := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "secondary failed"},
|
||||
})
|
||||
}
|
||||
tertiary := Of[string]("tertiary value")
|
||||
|
||||
combined := m.Concat(m.Concat(primary, secondary), tertiary)
|
||||
result := combined("input")
|
||||
|
||||
assert.Equal(t, validation.Of("tertiary value"), result)
|
||||
})
|
||||
|
||||
t.Run("satisfies monoid laws", func(t *testing.T) {
|
||||
m := AltMonoid(func() Decode[string, int] {
|
||||
return Of[string](0)
|
||||
})
|
||||
|
||||
decoder1 := Of[string](1)
|
||||
decoder2 := Of[string](2)
|
||||
decoder3 := Of[string](3)
|
||||
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
result := m.Concat(m.Empty(), decoder1)("input")
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return -1 },
|
||||
F.Identity[int],
|
||||
)
|
||||
// With AltMonoid, first success wins, so empty (0) is returned
|
||||
assert.Equal(t, 0, value)
|
||||
})
|
||||
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
result := m.Concat(decoder1, m.Empty())("input")
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return -1 },
|
||||
F.Identity[int],
|
||||
)
|
||||
// First decoder succeeds, so 1 is returned
|
||||
assert.Equal(t, 1, value)
|
||||
})
|
||||
|
||||
t.Run("associativity", func(t *testing.T) {
|
||||
// For AltMonoid, first success wins
|
||||
left := m.Concat(m.Concat(decoder1, decoder2), decoder3)("input")
|
||||
right := m.Concat(decoder1, m.Concat(decoder2, decoder3))("input")
|
||||
|
||||
leftVal := either.MonadFold(left,
|
||||
func(Errors) int { return -1 },
|
||||
F.Identity[int],
|
||||
)
|
||||
rightVal := either.MonadFold(right,
|
||||
func(Errors) int { return -1 },
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
// Both should return 1 (first success)
|
||||
assert.Equal(t, 1, leftVal)
|
||||
assert.Equal(t, 1, rightVal)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("difference from AlternativeMonoid", func(t *testing.T) {
|
||||
// AltMonoid - first success wins
|
||||
altM := AltMonoid(func() Decode[string, int] {
|
||||
return Of[string](0)
|
||||
})
|
||||
|
||||
// AlternativeMonoid - combines successes
|
||||
altMonoid := AlternativeMonoid[string](N.MonoidSum[int]())
|
||||
|
||||
decoder1 := Of[string](10)
|
||||
decoder2 := Of[string](32)
|
||||
|
||||
// AltMonoid: returns first success (10)
|
||||
result1 := altM.Concat(decoder1, decoder2)("input")
|
||||
value1 := either.MonadFold(result1,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 10, value1, "AltMonoid returns first success")
|
||||
|
||||
// AlternativeMonoid: combines both successes (10 + 32 = 42)
|
||||
result2 := altMonoid.Concat(decoder1, decoder2)("input")
|
||||
value2 := either.MonadFold(result2,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value2, "AlternativeMonoid combines successes")
|
||||
})
|
||||
|
||||
t.Run("error aggregation with context", func(t *testing.T) {
|
||||
m := AltMonoid(func() Decode[string, int] {
|
||||
return Of[string](0)
|
||||
})
|
||||
|
||||
failing1 := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{
|
||||
Value: input,
|
||||
Messsage: "parse error",
|
||||
Context: validation.Context{{Key: "field", Type: "int"}},
|
||||
},
|
||||
})
|
||||
}
|
||||
failing2 := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{
|
||||
Value: input,
|
||||
Messsage: "validation error",
|
||||
Context: validation.Context{{Key: "value", Type: "int"}},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
combined := m.Concat(failing1, failing2)
|
||||
result := combined("abc")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.GreaterOrEqual(t, len(errors), 2, "Should have errors from both decoders")
|
||||
|
||||
// Verify that errors with context are present
|
||||
hasParseError := false
|
||||
hasValidationError := false
|
||||
for _, err := range errors {
|
||||
if err.Messsage == "parse error" {
|
||||
hasParseError = true
|
||||
assert.NotNil(t, err.Context)
|
||||
}
|
||||
if err.Messsage == "validation error" {
|
||||
hasValidationError = true
|
||||
assert.NotNil(t, err.Context)
|
||||
}
|
||||
}
|
||||
assert.True(t, hasParseError, "Should have parse error")
|
||||
assert.True(t, hasValidationError, "Should have validation error")
|
||||
})
|
||||
}
|
||||
@@ -17,11 +17,60 @@ package decode
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
type (
|
||||
// Errors is a collection of validation errors that occurred during decoding.
|
||||
// This is an alias for validation.Errors, which is []*ValidationError.
|
||||
//
|
||||
// Errors accumulates multiple validation failures, allowing decoders to report
|
||||
// all problems at once rather than failing on the first error. This is particularly
|
||||
// useful for form validation, API request validation, and configuration parsing
|
||||
// where users benefit from seeing all issues simultaneously.
|
||||
//
|
||||
// The Errors type forms a Semigroup and Monoid, enabling:
|
||||
// - Concatenation: Combining errors from multiple decoders
|
||||
// - Accumulation: Collecting errors through applicative operations
|
||||
// - Empty value: An empty slice representing no errors (success)
|
||||
//
|
||||
// Each error in the collection is a *ValidationError containing:
|
||||
// - Value: The actual value that failed validation
|
||||
// - Context: The path to the value in nested structures
|
||||
// - Message: Human-readable error description
|
||||
// - Cause: Optional underlying error
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Multiple validation failures
|
||||
// errors := Errors{
|
||||
// &validation.ValidationError{
|
||||
// Value: "",
|
||||
// Context: []validation.ContextEntry{{Key: "name"}},
|
||||
// Messsage: "name is required",
|
||||
// },
|
||||
// &validation.ValidationError{
|
||||
// Value: "invalid@",
|
||||
// Context: []validation.ContextEntry{{Key: "email"}},
|
||||
// Messsage: "invalid email format",
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// // Create a failed validation with these errors
|
||||
// result := validation.Failures[User](errors)
|
||||
//
|
||||
// // Errors can be combined using the monoid
|
||||
// moreErrors := Errors{
|
||||
// &validation.ValidationError{
|
||||
// Value: -1,
|
||||
// Context: []validation.ContextEntry{{Key: "age"}},
|
||||
// Messsage: "age must be positive",
|
||||
// },
|
||||
// }
|
||||
// allErrors := append(errors, moreErrors...)
|
||||
Errors = validation.Errors
|
||||
|
||||
// Validation represents the result of a validation operation that may contain
|
||||
@@ -219,4 +268,79 @@ type (
|
||||
// LetL(nameLens, normalize),
|
||||
// )
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
// Monoid represents an algebraic structure with an associative binary operation
|
||||
// and an identity element. This is an alias for monoid.Monoid[A].
|
||||
//
|
||||
// A Monoid[A] consists of:
|
||||
// - Concat: func(A, A) A - An associative binary operation
|
||||
// - Empty: func() A - An identity element
|
||||
//
|
||||
// In the decode context, monoids are used to combine multiple decoders or
|
||||
// validation results. The most common use case is combining validation errors
|
||||
// from multiple decoders using the Errors monoid.
|
||||
//
|
||||
// Properties:
|
||||
// - Associativity: Concat(Concat(a, b), c) == Concat(a, Concat(b, c))
|
||||
// - Identity: Concat(Empty(), a) == a == Concat(a, Empty())
|
||||
//
|
||||
// Common monoid instances:
|
||||
// - Errors: Combines validation errors from multiple sources
|
||||
// - Array: Concatenates arrays of decoded values
|
||||
// - String: Concatenates strings
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Combine validation errors from multiple decoders
|
||||
// errorsMonoid := validation.GetMonoid[int]()
|
||||
//
|
||||
// // Decode multiple fields and combine errors
|
||||
// result1 := decodeField1(data) // Validation[string]
|
||||
// result2 := decodeField2(data) // Validation[int]
|
||||
//
|
||||
// // If both fail, errors are combined using the monoid
|
||||
// combined := errorsMonoid.Concat(result1, result2)
|
||||
//
|
||||
// // The monoid's Empty() provides a successful validation with no errors
|
||||
// empty := errorsMonoid.Empty() // Success with no value
|
||||
Monoid[A any] = monoid.Monoid[A]
|
||||
|
||||
// Lazy represents a deferred computation that produces a value of type A.
|
||||
// This is an alias for lazy.Lazy[A], which is func() A.
|
||||
//
|
||||
// In the decode context, Lazy is used to defer expensive computations or
|
||||
// recursive decoder definitions until they are actually needed. This is
|
||||
// particularly important for:
|
||||
// - Recursive data structures (e.g., trees, linked lists)
|
||||
// - Expensive default values
|
||||
// - Breaking circular dependencies in decoder definitions
|
||||
//
|
||||
// A Lazy[A] is simply a function that takes no arguments and returns A.
|
||||
// The computation is only executed when the function is called, allowing
|
||||
// for lazy evaluation and recursive definitions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Define a recursive decoder for a tree structure
|
||||
// type Tree struct {
|
||||
// Value int
|
||||
// Children []Tree
|
||||
// }
|
||||
//
|
||||
// // Use Lazy to break the circular dependency
|
||||
// var decodeTree Decode[map[string]any, Tree]
|
||||
// decodeTree = func(data map[string]any) Validation[Tree] {
|
||||
// // Lazy evaluation allows referencing decodeTree within itself
|
||||
// childrenDecoder := Array(Lazy(func() Decode[map[string]any, Tree] {
|
||||
// return decodeTree
|
||||
// }))
|
||||
// // ... rest of decoder implementation
|
||||
// }
|
||||
//
|
||||
// // Lazy default value that's only computed if needed
|
||||
// expensiveDefault := Lazy(func() Config {
|
||||
// // This computation only runs if the decode fails
|
||||
// return computeExpensiveDefaultConfig()
|
||||
// })
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
)
|
||||
|
||||
@@ -18,11 +18,10 @@ package codec
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validate"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
)
|
||||
|
||||
// encodeEither creates an encoder for Either[A, B] values.
|
||||
@@ -151,28 +150,20 @@ func validateEither[A, B, O, I any](
|
||||
rightItem Type[B, O, I],
|
||||
) Validate[I, either.Either[A, B]] {
|
||||
|
||||
return func(i I) Decode[Context, either.Either[A, B]] {
|
||||
valRight := rightItem.Validate(i)
|
||||
valLeft := leftItem.Validate(i)
|
||||
valRight := F.Pipe1(
|
||||
rightItem.Validate,
|
||||
validate.Map[I, B](either.Right[A]),
|
||||
)
|
||||
|
||||
return func(ctx Context) Validation[either.Either[A, B]] {
|
||||
valLeft := F.Pipe1(
|
||||
leftItem.Validate,
|
||||
validate.Map[I, A](either.Left[B]),
|
||||
)
|
||||
|
||||
resRight := valRight(ctx)
|
||||
|
||||
return either.Fold(
|
||||
func(rightErrors validate.Errors) Validation[either.Either[A, B]] {
|
||||
resLeft := valLeft(ctx)
|
||||
return either.Fold(
|
||||
func(leftErrors validate.Errors) Validation[either.Either[A, B]] {
|
||||
return validation.Failures[either.Either[A, B]](array.Concat(leftErrors)(rightErrors))
|
||||
},
|
||||
F.Flow2(either.Left[B, A], validation.Of),
|
||||
)(resLeft)
|
||||
},
|
||||
F.Flow2(either.Right[A, B], validation.Of),
|
||||
)(resRight)
|
||||
}
|
||||
}
|
||||
return F.Pipe1(
|
||||
valRight,
|
||||
validate.Alt(lazy.Of(valLeft)),
|
||||
)
|
||||
}
|
||||
|
||||
// Either creates a codec for Either[A, B] values.
|
||||
@@ -265,12 +256,9 @@ func Either[A, B, O, I any](
|
||||
leftItem Type[A, O, I],
|
||||
rightItem Type[B, O, I],
|
||||
) Type[either.Either[A, B], O, I] {
|
||||
name := fmt.Sprintf("Either[%s, %s]", leftItem.Name(), rightItem.Name())
|
||||
isEither := Is[either.Either[A, B]]()
|
||||
|
||||
return MakeType(
|
||||
name,
|
||||
isEither,
|
||||
fmt.Sprintf("Either[%s, %s]", leftItem.Name(), rightItem.Name()),
|
||||
Is[either.Either[A, B]](),
|
||||
validateEither(leftItem, rightItem),
|
||||
encodeEither(leftItem, rightItem),
|
||||
)
|
||||
|
||||
@@ -342,6 +342,27 @@ func TestEitherErrorAccumulation(t *testing.T) {
|
||||
|
||||
require.NotNil(t, errors)
|
||||
// Should have errors from both string and int validation attempts
|
||||
assert.NotEmpty(t, errors)
|
||||
assert.GreaterOrEqual(t, len(errors), 2, "Should have at least 2 errors (one from Right validation, one from Left validation)")
|
||||
|
||||
// Verify we have errors from both validation attempts
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
|
||||
// Check that we have errors related to both validations
|
||||
hasIntError := false
|
||||
hasStringError := false
|
||||
for _, msg := range messages {
|
||||
if msg == "expected integer string" || msg == "must be positive" {
|
||||
hasIntError = true
|
||||
}
|
||||
if msg == "must not be empty" {
|
||||
hasStringError = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, hasIntError, "Should have error from integer validation (Right branch)")
|
||||
assert.True(t, hasStringError, "Should have error from string validation (Left branch)")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,6 +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 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]
|
||||
)
|
||||
|
||||
661
v2/optics/codec/validate/monad_test.go
Normal file
661
v2/optics/codec/validate/monad_test.go
Normal file
@@ -0,0 +1,661 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestMonadChainLeft tests the MonadChainLeft function
|
||||
func TestMonadChainLeft(t *testing.T) {
|
||||
t.Run("transforms failures while preserving successes", func(t *testing.T) {
|
||||
// Create a failing validator
|
||||
failingValidator := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "validation failed"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Handler that recovers from specific errors
|
||||
handler := func(errs Errors) Validate[string, int] {
|
||||
for _, err := range errs {
|
||||
if err.Messsage == "validation failed" {
|
||||
return Of[string, int](0) // recover with default
|
||||
}
|
||||
}
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validator := MonadChainLeft(failingValidator, handler)
|
||||
res := validator("input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(0), res, "Should recover from failure")
|
||||
})
|
||||
|
||||
t.Run("preserves success values unchanged", func(t *testing.T) {
|
||||
successValidator := Of[string, int](42)
|
||||
|
||||
handler := func(errs Errors) Validate[string, int] {
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Messsage: "should not be called"},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validator := MonadChainLeft(successValidator, handler)
|
||||
res := validator("input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(42), res, "Success should pass through unchanged")
|
||||
})
|
||||
|
||||
t.Run("aggregates errors when transformation also fails", func(t *testing.T) {
|
||||
failingValidator := func(input string) Reader[Context, Validation[string]] {
|
||||
return func(ctx Context) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "original error"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handler := func(errs Errors) Validate[string, string] {
|
||||
return func(input string) Reader[Context, Validation[string]] {
|
||||
return func(ctx Context) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Messsage: "additional error"},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validator := MonadChainLeft(failingValidator, handler)
|
||||
res := validator("input")(nil)
|
||||
|
||||
assert.True(t, either.IsLeft(res))
|
||||
errors := either.MonadFold(res,
|
||||
reader.Ask[Errors](),
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2, "Should aggregate both errors")
|
||||
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "original error")
|
||||
assert.Contains(t, messages, "additional error")
|
||||
})
|
||||
|
||||
t.Run("adds context to errors", func(t *testing.T) {
|
||||
failingValidator := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "invalid format"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
addContext := func(errs Errors) Validate[string, int] {
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{
|
||||
Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
|
||||
Messsage: "failed to validate user age",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validator := MonadChainLeft(failingValidator, addContext)
|
||||
res := validator("abc")(nil)
|
||||
|
||||
assert.True(t, either.IsLeft(res))
|
||||
errors := either.MonadFold(res,
|
||||
reader.Ask[Errors](),
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2, "Should have both original and context errors")
|
||||
})
|
||||
|
||||
t.Run("works with different input types", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Port int
|
||||
}
|
||||
|
||||
failingValidator := func(cfg Config) Reader[Context, Validation[string]] {
|
||||
return func(ctx Context) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: cfg.Port, Messsage: "invalid port"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handler := func(errs Errors) Validate[Config, string] {
|
||||
return Of[Config, string]("default-value")
|
||||
}
|
||||
|
||||
validator := MonadChainLeft(failingValidator, handler)
|
||||
res := validator(Config{Port: 9999})(nil)
|
||||
|
||||
assert.Equal(t, validation.Of("default-value"), res)
|
||||
})
|
||||
|
||||
t.Run("handler can access original input", func(t *testing.T) {
|
||||
failingValidator := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "parse failed"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handler := func(errs Errors) Validate[string, int] {
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
// Handler can use the original input to make decisions
|
||||
if input == "special" {
|
||||
return validation.Of(999)
|
||||
}
|
||||
return validation.Of(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validator := MonadChainLeft(failingValidator, handler)
|
||||
|
||||
res1 := validator("special")(nil)
|
||||
assert.Equal(t, validation.Of(999), res1)
|
||||
|
||||
res2 := validator("other")(nil)
|
||||
assert.Equal(t, validation.Of(0), res2)
|
||||
})
|
||||
|
||||
t.Run("is equivalent to ChainLeft", func(t *testing.T) {
|
||||
failingValidator := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handler := func(errs Errors) Validate[string, int] {
|
||||
return Of[string, int](42)
|
||||
}
|
||||
|
||||
// MonadChainLeft - direct application
|
||||
result1 := MonadChainLeft(failingValidator, handler)("input")(nil)
|
||||
|
||||
// ChainLeft - curried for pipelines
|
||||
result2 := ChainLeft(handler)(failingValidator)("input")(nil)
|
||||
|
||||
assert.Equal(t, result1, result2, "MonadChainLeft and ChainLeft should produce identical results")
|
||||
})
|
||||
|
||||
t.Run("chains multiple error transformations", func(t *testing.T) {
|
||||
failingValidator := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error1"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handler1 := func(errs Errors) Validate[string, int] {
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Messsage: "error2"},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handler2 := func(errs Errors) Validate[string, int] {
|
||||
// Check if we can recover
|
||||
for _, err := range errs {
|
||||
if err.Messsage == "error1" {
|
||||
return Of[string, int](100) // recover
|
||||
}
|
||||
}
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chain handlers
|
||||
validator := MonadChainLeft(MonadChainLeft(failingValidator, handler1), handler2)
|
||||
res := validator("input")(nil)
|
||||
|
||||
// Should recover because error1 is present
|
||||
assert.Equal(t, validation.Of(100), res)
|
||||
})
|
||||
|
||||
t.Run("does not call handler on success", func(t *testing.T) {
|
||||
successValidator := Of[string, int](42)
|
||||
handlerCalled := false
|
||||
|
||||
handler := func(errs Errors) Validate[string, int] {
|
||||
handlerCalled = true
|
||||
return Of[string, int](0)
|
||||
}
|
||||
|
||||
validator := MonadChainLeft(successValidator, handler)
|
||||
res := validator("input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(42), res)
|
||||
assert.False(t, handlerCalled, "Handler should not be called on success")
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadAlt tests the MonadAlt function
|
||||
func TestMonadAlt(t *testing.T) {
|
||||
t.Run("returns first validator when it succeeds", func(t *testing.T) {
|
||||
validator1 := Of[string, int](42)
|
||||
validator2 := func() Validate[string, int] {
|
||||
return Of[string, int](100)
|
||||
}
|
||||
|
||||
result := MonadAlt(validator1, validator2)("input")(nil)
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("returns second validator when first fails", func(t *testing.T) {
|
||||
failing := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "first failed"},
|
||||
})
|
||||
}
|
||||
}
|
||||
fallback := func() Validate[string, int] {
|
||||
return Of[string, int](42)
|
||||
}
|
||||
|
||||
result := MonadAlt(failing, fallback)("input")(nil)
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("aggregates errors when both fail", func(t *testing.T) {
|
||||
failing1 := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 1"},
|
||||
})
|
||||
}
|
||||
}
|
||||
failing2 := func() Validate[string, int] {
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 2"},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := MonadAlt(failing1, failing2)("input")(nil)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
reader.Ask[Errors](),
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors from both validators")
|
||||
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "error 1", "Should contain error from first validator")
|
||||
assert.Contains(t, messages, "error 2", "Should contain error from second validator")
|
||||
})
|
||||
|
||||
t.Run("does not evaluate second validator when first succeeds", func(t *testing.T) {
|
||||
validator1 := Of[string, int](42)
|
||||
evaluated := false
|
||||
validator2 := func() Validate[string, int] {
|
||||
evaluated = true
|
||||
return Of[string, int](100)
|
||||
}
|
||||
|
||||
result := MonadAlt(validator1, validator2)("input")(nil)
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
assert.False(t, evaluated, "Second validator should not be evaluated")
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
failing := func(input string) Reader[Context, Validation[string]] {
|
||||
return func(ctx Context) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "failed"},
|
||||
})
|
||||
}
|
||||
}
|
||||
fallback := func() Validate[string, string] {
|
||||
return Of[string, string]("fallback")
|
||||
}
|
||||
|
||||
result := MonadAlt(failing, fallback)("input")(nil)
|
||||
assert.Equal(t, validation.Of("fallback"), result)
|
||||
})
|
||||
|
||||
t.Run("chains multiple alternatives", func(t *testing.T) {
|
||||
failing1 := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 1"},
|
||||
})
|
||||
}
|
||||
}
|
||||
failing2 := func() Validate[string, int] {
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 2"},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
succeeding := func() Validate[string, int] {
|
||||
return Of[string, int](42)
|
||||
}
|
||||
|
||||
// Chain: try failing1, then failing2, then succeeding
|
||||
result := MonadAlt(MonadAlt(failing1, failing2), succeeding)("input")(nil)
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("works with complex input types", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Port int
|
||||
}
|
||||
|
||||
failing := func(cfg Config) Reader[Context, Validation[string]] {
|
||||
return func(ctx Context) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: cfg.Port, Messsage: "invalid port"},
|
||||
})
|
||||
}
|
||||
}
|
||||
fallback := func() Validate[Config, string] {
|
||||
return Of[Config, string]("default")
|
||||
}
|
||||
|
||||
result := MonadAlt(failing, fallback)(Config{Port: 9999})(nil)
|
||||
assert.Equal(t, validation.Of("default"), result)
|
||||
})
|
||||
|
||||
t.Run("preserves error context", func(t *testing.T) {
|
||||
failing1 := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{
|
||||
Value: input,
|
||||
Messsage: "parse error",
|
||||
Context: validation.Context{{Key: "field", Type: "int"}},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
failing2 := func() Validate[string, int] {
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{
|
||||
Value: input,
|
||||
Messsage: "validation error",
|
||||
Context: validation.Context{{Key: "value", Type: "int"}},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := MonadAlt(failing1, failing2)("abc")(nil)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
reader.Ask[Errors](),
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.GreaterOrEqual(t, len(errors), 2, "Should have errors from both validators")
|
||||
|
||||
// Verify that errors with context are present
|
||||
hasParseError := false
|
||||
hasValidationError := false
|
||||
for _, err := range errors {
|
||||
if err.Messsage == "parse error" {
|
||||
hasParseError = true
|
||||
assert.NotNil(t, err.Context)
|
||||
}
|
||||
if err.Messsage == "validation error" {
|
||||
hasValidationError = true
|
||||
assert.NotNil(t, err.Context)
|
||||
}
|
||||
}
|
||||
assert.True(t, hasParseError, "Should have parse error")
|
||||
assert.True(t, hasValidationError, "Should have validation error")
|
||||
})
|
||||
}
|
||||
|
||||
// TestAlt tests the Alt function
|
||||
func TestAlt(t *testing.T) {
|
||||
t.Run("returns first validator when it succeeds", func(t *testing.T) {
|
||||
validator1 := Of[string, int](42)
|
||||
validator2 := func() Validate[string, int] {
|
||||
return Of[string, int](100)
|
||||
}
|
||||
|
||||
withAlt := Alt(validator2)
|
||||
result := withAlt(validator1)("input")(nil)
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("returns second validator when first fails", func(t *testing.T) {
|
||||
failing := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "first failed"},
|
||||
})
|
||||
}
|
||||
}
|
||||
fallback := func() Validate[string, int] {
|
||||
return Of[string, int](42)
|
||||
}
|
||||
|
||||
withAlt := Alt(fallback)
|
||||
result := withAlt(failing)("input")(nil)
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("aggregates errors when both fail", func(t *testing.T) {
|
||||
failing1 := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 1"},
|
||||
})
|
||||
}
|
||||
}
|
||||
failing2 := func() Validate[string, int] {
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 2"},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
withAlt := Alt(failing2)
|
||||
result := withAlt(failing1)("input")(nil)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
reader.Ask[Errors](),
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors from both validators")
|
||||
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "error 1")
|
||||
assert.Contains(t, messages, "error 2")
|
||||
})
|
||||
|
||||
t.Run("does not evaluate second validator when first succeeds", func(t *testing.T) {
|
||||
validator1 := Of[string, int](42)
|
||||
evaluated := false
|
||||
validator2 := func() Validate[string, int] {
|
||||
evaluated = true
|
||||
return Of[string, int](100)
|
||||
}
|
||||
|
||||
withAlt := Alt(validator2)
|
||||
result := withAlt(validator1)("input")(nil)
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
assert.False(t, evaluated, "Second validator should not be evaluated")
|
||||
})
|
||||
|
||||
t.Run("can be used in pipelines", func(t *testing.T) {
|
||||
failing1 := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 1"},
|
||||
})
|
||||
}
|
||||
}
|
||||
failing2 := func() Validate[string, int] {
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 2"},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
succeeding := func() Validate[string, int] {
|
||||
return Of[string, int](42)
|
||||
}
|
||||
|
||||
// Use F.Pipe to chain alternatives
|
||||
validator := F.Pipe2(
|
||||
failing1,
|
||||
Alt(failing2),
|
||||
Alt(succeeding),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("is equivalent to MonadAlt", func(t *testing.T) {
|
||||
failing := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error"},
|
||||
})
|
||||
}
|
||||
}
|
||||
fallback := func() Validate[string, int] {
|
||||
return Of[string, int](42)
|
||||
}
|
||||
|
||||
// Alt - curried for pipelines
|
||||
result1 := Alt(fallback)(failing)("input")(nil)
|
||||
|
||||
// MonadAlt - direct application
|
||||
result2 := MonadAlt(failing, fallback)("input")(nil)
|
||||
|
||||
assert.Equal(t, result1, result2, "Alt and MonadAlt should produce identical results")
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadAltAndAltEquivalence tests that MonadAlt and Alt are equivalent
|
||||
func TestMonadAltAndAltEquivalence(t *testing.T) {
|
||||
t.Run("both produce same results for success", func(t *testing.T) {
|
||||
validator1 := Of[string, int](42)
|
||||
validator2 := func() Validate[string, int] {
|
||||
return Of[string, int](100)
|
||||
}
|
||||
|
||||
resultMonadAlt := MonadAlt(validator1, validator2)("input")(nil)
|
||||
resultAlt := Alt(validator2)(validator1)("input")(nil)
|
||||
|
||||
assert.Equal(t, resultMonadAlt, resultAlt)
|
||||
})
|
||||
|
||||
t.Run("both produce same results for fallback", func(t *testing.T) {
|
||||
failing := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "failed"},
|
||||
})
|
||||
}
|
||||
}
|
||||
fallback := func() Validate[string, int] {
|
||||
return Of[string, int](42)
|
||||
}
|
||||
|
||||
resultMonadAlt := MonadAlt(failing, fallback)("input")(nil)
|
||||
resultAlt := Alt(fallback)(failing)("input")(nil)
|
||||
|
||||
assert.Equal(t, resultMonadAlt, resultAlt)
|
||||
})
|
||||
|
||||
t.Run("both produce same results for error aggregation", func(t *testing.T) {
|
||||
failing1 := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 1"},
|
||||
})
|
||||
}
|
||||
}
|
||||
failing2 := func() Validate[string, int] {
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 2"},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resultMonadAlt := MonadAlt(failing1, failing2)("input")(nil)
|
||||
resultAlt := Alt(failing2)(failing1)("input")(nil)
|
||||
|
||||
// Both should fail
|
||||
assert.True(t, either.IsLeft(resultMonadAlt))
|
||||
assert.True(t, either.IsLeft(resultAlt))
|
||||
|
||||
// Both should have same errors
|
||||
errorsMonadAlt := either.MonadFold(resultMonadAlt,
|
||||
reader.Ask[Errors](),
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
errorsAlt := either.MonadFold(resultAlt,
|
||||
reader.Ask[Errors](),
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
|
||||
assert.Equal(t, len(errorsMonadAlt), len(errorsAlt))
|
||||
})
|
||||
}
|
||||
@@ -122,3 +122,268 @@ func ApplicativeMonoid[I, A any](m Monoid[A]) Monoid[Validate[I, A]] {
|
||||
m,
|
||||
)
|
||||
}
|
||||
|
||||
// AlternativeMonoid creates a Monoid instance for Validate[I, A] that combines both
|
||||
// applicative and alternative semantics.
|
||||
//
|
||||
// This function creates a monoid that:
|
||||
// 1. When both validators succeed: Combines their results using the provided monoid operation
|
||||
// 2. When one validator fails: Uses the successful validator's result (alternative behavior)
|
||||
// 3. When both validators fail: Aggregates all errors from both validators
|
||||
//
|
||||
// This is a hybrid approach that combines:
|
||||
// - ApplicativeMonoid: Combines successful results using the monoid operation
|
||||
// - AltMonoid: Provides fallback behavior when validators fail
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - I: The input type that validators accept
|
||||
// - A: The output type that validators produce (must have a Monoid instance)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - m: A Monoid[A] that defines how to combine values of type A
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A Monoid[Validate[I, A]] that combines validators using both applicative and alternative semantics.
|
||||
//
|
||||
// # Behavior Details
|
||||
//
|
||||
// The AlternativeMonoid differs from ApplicativeMonoid in how it handles mixed success/failure:
|
||||
//
|
||||
// - **Both succeed**: Results are combined using the monoid operation (like ApplicativeMonoid)
|
||||
// - **First succeeds, second fails**: Returns the first result (alternative fallback)
|
||||
// - **First fails, second succeeds**: Returns the second result (alternative fallback)
|
||||
// - **Both fail**: Aggregates errors from both validators
|
||||
//
|
||||
// # Example: String Concatenation with Fallback
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/optics/codec/validate"
|
||||
// "github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
// S "github.com/IBM/fp-go/v2/string"
|
||||
// )
|
||||
//
|
||||
// m := validate.AlternativeMonoid[string, string](S.Monoid)
|
||||
//
|
||||
// // Both succeed - results are concatenated
|
||||
// validator1 := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// return validation.Success("Hello")
|
||||
// }
|
||||
// }
|
||||
// validator2 := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// return validation.Success(" World")
|
||||
// }
|
||||
// }
|
||||
// combined := m.Concat(validator1, validator2)
|
||||
// result := combined("input")(nil)
|
||||
// // result is validation.Success("Hello World")
|
||||
//
|
||||
// # Example: Fallback Behavior
|
||||
//
|
||||
// // First fails, second succeeds - uses second result
|
||||
// failing := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// return validation.FailureWithMessage[string](input, "first failed")(ctx)
|
||||
// }
|
||||
// }
|
||||
// succeeding := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// return validation.Success("fallback")
|
||||
// }
|
||||
// }
|
||||
// combined := m.Concat(failing, succeeding)
|
||||
// result := combined("input")(nil)
|
||||
// // result is validation.Success("fallback")
|
||||
//
|
||||
// # Example: Error Aggregation
|
||||
//
|
||||
// // Both fail - errors are aggregated
|
||||
// failing1 := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// return validation.FailureWithMessage[string](input, "error 1")(ctx)
|
||||
// }
|
||||
// }
|
||||
// failing2 := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// return validation.FailureWithMessage[string](input, "error 2")(ctx)
|
||||
// }
|
||||
// }
|
||||
// combined := m.Concat(failing1, failing2)
|
||||
// result := combined("input")(nil)
|
||||
// // result contains both "error 1" and "error 2"
|
||||
//
|
||||
// # Comparison with Other Monoids
|
||||
//
|
||||
// - **ApplicativeMonoid**: Always combines results when both succeed, fails if either fails
|
||||
// - **AlternativeMonoid**: Combines results when both succeed, provides fallback when one fails
|
||||
// - **AltMonoid**: Always uses first success, never combines results
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Validation with fallback strategies and result combination
|
||||
// - Building validators that accumulate results but provide alternatives
|
||||
// - Configuration loading with multiple sources and merging
|
||||
// - Data aggregation with error recovery
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - Both validators receive the same input value I
|
||||
// - The empty element of the monoid serves as the identity for the Concat operation
|
||||
// - Error aggregation ensures no validation failures are lost
|
||||
// - This follows both applicative and alternative functor laws
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - ApplicativeMonoid: For pure applicative combination without fallback
|
||||
// - AltMonoid: For pure alternative behavior without result combination
|
||||
// - MonadAlt: The underlying alternative operation
|
||||
func AlternativeMonoid[I, A any](m Monoid[A]) Monoid[Validate[I, A]] {
|
||||
return monoid.AlternativeMonoid(
|
||||
Of[I, A],
|
||||
MonadMap[I, A, func(A) A],
|
||||
MonadAp[A, I, A],
|
||||
MonadAlt[I, A],
|
||||
m,
|
||||
)
|
||||
}
|
||||
|
||||
// AltMonoid creates a Monoid instance for Validate[I, A] using alternative semantics
|
||||
// with a provided zero/default validator.
|
||||
//
|
||||
// This function creates a monoid where:
|
||||
// 1. The first successful validator wins (no result combination)
|
||||
// 2. If the first fails, the second is tried as a fallback
|
||||
// 3. If both fail, errors are aggregated
|
||||
// 4. The provided zero validator serves as the identity element
|
||||
//
|
||||
// Unlike AlternativeMonoid, AltMonoid does NOT combine successful results - it always
|
||||
// returns the first success. This makes it ideal for fallback chains and default values.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - I: The input type that validators accept
|
||||
// - A: The output type that validators produce
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - zero: A lazy Validate[I, A] that serves as the identity element. This is typically
|
||||
// a validator that always succeeds with a default value, but can also be a failing
|
||||
// validator if no default is appropriate.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A Monoid[Validate[I, A]] that combines validators 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 validator is used as fallback
|
||||
//
|
||||
// # Example: Default Value Fallback
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/optics/codec/validate"
|
||||
// )
|
||||
//
|
||||
// // Create a monoid with a default value of 0
|
||||
// m := validate.AltMonoid(func() validate.Validate[string, int] {
|
||||
// return validate.Of[string, int](0)
|
||||
// })
|
||||
//
|
||||
// // First validator succeeds - returns 42, second is not evaluated
|
||||
// validator1 := validate.Of[string, int](42)
|
||||
// validator2 := validate.Of[string, int](100)
|
||||
// combined := m.Concat(validator1, validator2)
|
||||
// result := combined("input")(nil)
|
||||
// // result is validation.Success(42)
|
||||
//
|
||||
// # Example: Fallback Chain
|
||||
//
|
||||
// // Try primary, then fallback, then default
|
||||
// m := validate.AltMonoid(func() validate.Validate[string, string] {
|
||||
// return validate.Of[string, string]("default")
|
||||
// })
|
||||
//
|
||||
// primary := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// return validation.FailureWithMessage[string](input, "primary failed")(ctx)
|
||||
// }
|
||||
// }
|
||||
// secondary := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// return validation.Success("secondary value")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Chain: try primary, then secondary, then default
|
||||
// combined := m.Concat(m.Concat(primary, secondary), m.Empty())
|
||||
// result := combined("input")(nil)
|
||||
// // result is validation.Success("secondary value")
|
||||
//
|
||||
// # Example: Error Aggregation
|
||||
//
|
||||
// // Both fail - errors are aggregated
|
||||
// m := validate.AltMonoid(func() validate.Validate[string, int] {
|
||||
// return func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
|
||||
// return func(ctx validation.Context) validation.Validation[int] {
|
||||
// return validation.FailureWithMessage[int](input, "no default")(ctx)
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// failing1 := func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
|
||||
// return func(ctx validation.Context) validation.Validation[int] {
|
||||
// return validation.FailureWithMessage[int](input, "error 1")(ctx)
|
||||
// }
|
||||
// }
|
||||
// failing2 := func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
|
||||
// return func(ctx validation.Context) validation.Validation[int] {
|
||||
// return validation.FailureWithMessage[int](input, "error 2")(ctx)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// combined := m.Concat(failing1, failing2)
|
||||
// result := combined("input")(nil)
|
||||
// // result contains both "error 1" and "error 2"
|
||||
//
|
||||
// # Comparison with Other Monoids
|
||||
//
|
||||
// - **ApplicativeMonoid**: Combines results when both succeed using monoid operation
|
||||
// - **AlternativeMonoid**: Combines results when both succeed, provides fallback when one fails
|
||||
// - **AltMonoid**: First success wins, never combines results (pure alternative)
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Configuration loading with fallback sources (try file, then env, then default)
|
||||
// - Validation with default values
|
||||
// - Parser combinators with alternative branches
|
||||
// - Error recovery with multiple strategies
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - The zero validator is lazily evaluated, only when needed
|
||||
// - First success short-circuits evaluation (second validator not called)
|
||||
// - Error aggregation ensures all validation failures are reported
|
||||
// - This follows the alternative functor laws
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - AlternativeMonoid: For combining results when both succeed
|
||||
// - ApplicativeMonoid: For pure applicative combination
|
||||
// - MonadAlt: The underlying alternative operation
|
||||
// - Alt: The curried version for pipeline composition
|
||||
func AltMonoid[I, A any](zero Lazy[Validate[I, A]]) Monoid[Validate[I, A]] {
|
||||
return monoid.AltMonoid(
|
||||
zero,
|
||||
MonadAlt[I, A],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,475 +1,397 @@
|
||||
// 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 validate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
MO "github.com/IBM/fp-go/v2/monoid"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
intAddMonoid = N.MonoidSum[int]()
|
||||
strMonoid = S.Monoid
|
||||
)
|
||||
// TestAlternativeMonoid tests the AlternativeMonoid function
|
||||
func TestAlternativeMonoid(t *testing.T) {
|
||||
t.Run("with string monoid", func(t *testing.T) {
|
||||
m := AlternativeMonoid[string, string](S.Monoid)
|
||||
|
||||
// Helper function to create a successful validator
|
||||
func successValidator[I, A any](value A) Validate[I, A] {
|
||||
return func(input I) Reader[validation.Context, validation.Validation[A]] {
|
||||
return func(ctx validation.Context) validation.Validation[A] {
|
||||
return validation.Success(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Run("empty returns validator that succeeds with empty string", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
result := empty("input")(nil)
|
||||
|
||||
// Helper function to create a failing validator
|
||||
func failureValidator[I, A any](message string) Validate[I, A] {
|
||||
return func(input I) Reader[validation.Context, validation.Validation[A]] {
|
||||
return validation.FailureWithMessage[A](input, message)
|
||||
}
|
||||
}
|
||||
assert.Equal(t, validation.Of(""), result)
|
||||
})
|
||||
|
||||
// Helper function to create a validator that uses the input
|
||||
func inputDependentValidator[A any](f func(A) A) Validate[A, A] {
|
||||
return func(input A) Reader[validation.Context, validation.Validation[A]] {
|
||||
return func(ctx validation.Context) validation.Validation[A] {
|
||||
return validation.Success(f(input))
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Run("concat combines successful validators using monoid", func(t *testing.T) {
|
||||
validator1 := Of[string, string]("Hello")
|
||||
validator2 := Of[string, string](" World")
|
||||
|
||||
// TestApplicativeMonoid_EmptyElement tests the empty element of the monoid
|
||||
func TestApplicativeMonoid_EmptyElement(t *testing.T) {
|
||||
t.Run("int addition monoid", func(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
empty := m.Empty()
|
||||
combined := m.Concat(validator1, validator2)
|
||||
result := combined("input")(nil)
|
||||
|
||||
result := empty("test")(nil)
|
||||
assert.Equal(t, validation.Of("Hello World"), result)
|
||||
})
|
||||
|
||||
assert.Equal(t, validation.Of(0), result)
|
||||
})
|
||||
|
||||
t.Run("string concatenation monoid", func(t *testing.T) {
|
||||
m := ApplicativeMonoid[int](strMonoid)
|
||||
empty := m.Empty()
|
||||
|
||||
result := empty(42)(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(""), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_ConcatSuccesses tests concatenating two successful validators
|
||||
func TestApplicativeMonoid_ConcatSuccesses(t *testing.T) {
|
||||
t.Run("int addition", func(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
v1 := successValidator[string](5)
|
||||
v2 := successValidator[string](3)
|
||||
|
||||
combined := m.Concat(v1, v2)
|
||||
result := combined("input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(8), result)
|
||||
})
|
||||
|
||||
t.Run("string concatenation", func(t *testing.T) {
|
||||
m := ApplicativeMonoid[int](strMonoid)
|
||||
|
||||
v1 := successValidator[int]("Hello")
|
||||
v2 := successValidator[int](" World")
|
||||
|
||||
combined := m.Concat(v1, v2)
|
||||
result := combined(42)(nil)
|
||||
|
||||
assert.Equal(t, validation.Of("Hello World"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_ConcatWithFailure tests concatenating validators where one fails
|
||||
func TestApplicativeMonoid_ConcatWithFailure(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
t.Run("left failure", func(t *testing.T) {
|
||||
v1 := failureValidator[string, int]("left error")
|
||||
v2 := successValidator[string](5)
|
||||
|
||||
combined := m.Concat(v1, v2)
|
||||
result := combined("input")(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "left error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("right failure", func(t *testing.T) {
|
||||
v1 := successValidator[string](5)
|
||||
v2 := failureValidator[string, int]("right error")
|
||||
|
||||
combined := m.Concat(v1, v2)
|
||||
result := combined("input")(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "right error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("both failures", func(t *testing.T) {
|
||||
v1 := failureValidator[string, int]("left error")
|
||||
v2 := failureValidator[string, int]("right error")
|
||||
|
||||
combined := m.Concat(v1, v2)
|
||||
result := combined("input")(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
// Note: The current implementation returns the first error encountered
|
||||
assert.GreaterOrEqual(t, len(errors), 1)
|
||||
// At least one of the errors should be present
|
||||
hasError := false
|
||||
for _, err := range errors {
|
||||
if err.Messsage == "left error" || err.Messsage == "right error" {
|
||||
hasError = true
|
||||
break
|
||||
t.Run("concat uses second as fallback when first fails", func(t *testing.T) {
|
||||
failing := func(input string) Reader[Context, Validation[string]] {
|
||||
return func(ctx Context) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "first failed"},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.True(t, hasError, "Should contain at least one validation error")
|
||||
})
|
||||
}
|
||||
succeeding := Of[string, string]("fallback")
|
||||
|
||||
// TestApplicativeMonoid_LeftIdentity tests the left identity law
|
||||
func TestApplicativeMonoid_LeftIdentity(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
combined := m.Concat(failing, succeeding)
|
||||
result := combined("input")(nil)
|
||||
|
||||
v := successValidator[string](42)
|
||||
assert.Equal(t, validation.Of("fallback"), result)
|
||||
})
|
||||
|
||||
// empty <> v == v
|
||||
combined := m.Concat(m.Empty(), v)
|
||||
|
||||
resultCombined := combined("test")(nil)
|
||||
resultOriginal := v("test")(nil)
|
||||
|
||||
assert.Equal(t, resultOriginal, resultCombined)
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_RightIdentity tests the right identity law
|
||||
func TestApplicativeMonoid_RightIdentity(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
v := successValidator[string](42)
|
||||
|
||||
// v <> empty == v
|
||||
combined := m.Concat(v, m.Empty())
|
||||
|
||||
resultCombined := combined("test")(nil)
|
||||
resultOriginal := v("test")(nil)
|
||||
|
||||
assert.Equal(t, resultOriginal, resultCombined)
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_Associativity tests the associativity law
|
||||
func TestApplicativeMonoid_Associativity(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
v1 := successValidator[string](1)
|
||||
v2 := successValidator[string](2)
|
||||
v3 := successValidator[string](3)
|
||||
|
||||
// (v1 <> v2) <> v3 == v1 <> (v2 <> v3)
|
||||
left := m.Concat(m.Concat(v1, v2), v3)
|
||||
right := m.Concat(v1, m.Concat(v2, v3))
|
||||
|
||||
resultLeft := left("test")(nil)
|
||||
resultRight := right("test")(nil)
|
||||
|
||||
assert.Equal(t, resultRight, resultLeft)
|
||||
|
||||
// Both should equal 6
|
||||
assert.Equal(t, validation.Of(6), resultLeft)
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_AssociativityWithFailures tests associativity with failures
|
||||
func TestApplicativeMonoid_AssociativityWithFailures(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
v1 := successValidator[string](1)
|
||||
v2 := failureValidator[string, int]("error 2")
|
||||
v3 := successValidator[string](3)
|
||||
|
||||
// (v1 <> v2) <> v3 == v1 <> (v2 <> v3)
|
||||
left := m.Concat(m.Concat(v1, v2), v3)
|
||||
right := m.Concat(v1, m.Concat(v2, v3))
|
||||
|
||||
resultLeft := left("test")(nil)
|
||||
resultRight := right("test")(nil)
|
||||
|
||||
// Both should fail with the same error
|
||||
assert.True(t, E.IsLeft(resultLeft))
|
||||
assert.True(t, E.IsLeft(resultRight))
|
||||
|
||||
_, errorsLeft := E.Unwrap(resultLeft)
|
||||
_, errorsRight := E.Unwrap(resultRight)
|
||||
|
||||
assert.Len(t, errorsLeft, 1)
|
||||
assert.Len(t, errorsRight, 1)
|
||||
assert.Equal(t, "error 2", errorsLeft[0].Messsage)
|
||||
assert.Equal(t, "error 2", errorsRight[0].Messsage)
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_MultipleValidators tests combining multiple validators
|
||||
func TestApplicativeMonoid_MultipleValidators(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
v1 := successValidator[string](10)
|
||||
v2 := successValidator[string](20)
|
||||
v3 := successValidator[string](30)
|
||||
v4 := successValidator[string](40)
|
||||
|
||||
// Chain multiple concat operations
|
||||
combined := m.Concat(
|
||||
m.Concat(
|
||||
m.Concat(v1, v2),
|
||||
v3,
|
||||
),
|
||||
v4,
|
||||
)
|
||||
|
||||
result := combined("test")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(100), result)
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_InputDependent tests validators that depend on input
|
||||
func TestApplicativeMonoid_InputDependent(t *testing.T) {
|
||||
m := ApplicativeMonoid[int](intAddMonoid)
|
||||
|
||||
// Validator that doubles the input
|
||||
v1 := inputDependentValidator(N.Mul(2))
|
||||
// Validator that adds 10 to the input
|
||||
v2 := inputDependentValidator(N.Add(10))
|
||||
|
||||
combined := m.Concat(v1, v2)
|
||||
result := combined(5)(nil)
|
||||
|
||||
// (5 * 2) + (5 + 10) = 10 + 15 = 25
|
||||
assert.Equal(t, validation.Of(25), result)
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_ContextPropagation tests that context is properly propagated
|
||||
func TestApplicativeMonoid_ContextPropagation(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
// Create a validator that captures the context
|
||||
var capturedContext validation.Context
|
||||
v1 := func(input string) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
capturedContext = ctx
|
||||
return validation.Success(5)
|
||||
}
|
||||
}
|
||||
|
||||
v2 := successValidator[string](3)
|
||||
|
||||
combined := m.Concat(v1, v2)
|
||||
|
||||
// Create a context with some entries
|
||||
ctx := validation.Context{
|
||||
{Key: "field1", Type: "int"},
|
||||
{Key: "field2", Type: "string"},
|
||||
}
|
||||
|
||||
result := combined("test")(ctx)
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
assert.Equal(t, ctx, capturedContext)
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_ErrorAccumulation tests that errors are accumulated
|
||||
func TestApplicativeMonoid_ErrorAccumulation(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
v1 := failureValidator[string, int]("error 1")
|
||||
v2 := failureValidator[string, int]("error 2")
|
||||
v3 := failureValidator[string, int]("error 3")
|
||||
|
||||
combined := m.Concat(m.Concat(v1, v2), v3)
|
||||
result := combined("test")(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
|
||||
// Note: The current implementation returns the first error encountered
|
||||
// At least one error should be present
|
||||
assert.GreaterOrEqual(t, len(errors), 1)
|
||||
hasError := false
|
||||
for _, err := range errors {
|
||||
if err.Messsage == "error 1" || err.Messsage == "error 2" || err.Messsage == "error 3" {
|
||||
hasError = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, hasError, "Should contain at least one validation error")
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_MixedSuccessFailure tests mixing successes and failures
|
||||
func TestApplicativeMonoid_MixedSuccessFailure(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
v1 := successValidator[string](10)
|
||||
v2 := failureValidator[string, int]("error in v2")
|
||||
v3 := successValidator[string](20)
|
||||
v4 := failureValidator[string, int]("error in v4")
|
||||
|
||||
combined := m.Concat(
|
||||
m.Concat(
|
||||
m.Concat(v1, v2),
|
||||
v3,
|
||||
),
|
||||
v4,
|
||||
)
|
||||
|
||||
result := combined("test")(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
|
||||
// Note: The current implementation returns the first error encountered
|
||||
// At least one error should be present
|
||||
assert.GreaterOrEqual(t, len(errors), 1)
|
||||
hasError := false
|
||||
for _, err := range errors {
|
||||
if err.Messsage == "error in v2" || err.Messsage == "error in v4" {
|
||||
hasError = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, hasError, "Should contain at least one validation error")
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_DifferentInputTypes tests with different input types
|
||||
func TestApplicativeMonoid_DifferentInputTypes(t *testing.T) {
|
||||
t.Run("struct input", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Port int
|
||||
Timeout int
|
||||
}
|
||||
|
||||
m := ApplicativeMonoid[Config](intAddMonoid)
|
||||
|
||||
v1 := func(cfg Config) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.Success(cfg.Port)
|
||||
t.Run("concat aggregates errors when both fail", func(t *testing.T) {
|
||||
failing1 := func(input string) Reader[Context, Validation[string]] {
|
||||
return func(ctx Context) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "error 1"},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
v2 := func(cfg Config) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.Success(cfg.Timeout)
|
||||
failing2 := func(input string) Reader[Context, Validation[string]] {
|
||||
return func(ctx Context) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "error 2"},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
combined := m.Concat(v1, v2)
|
||||
result := combined(Config{Port: 8080, Timeout: 30})(nil)
|
||||
combined := m.Concat(failing1, failing2)
|
||||
result := combined("input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(8110), result) // 8080 + 30
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors from both validators")
|
||||
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "error 1")
|
||||
assert.Contains(t, messages, "error 2")
|
||||
})
|
||||
|
||||
t.Run("concat with empty preserves validator", func(t *testing.T) {
|
||||
validator := Of[string, string]("test")
|
||||
empty := m.Empty()
|
||||
|
||||
result1 := m.Concat(validator, empty)("input")(nil)
|
||||
result2 := m.Concat(empty, validator)("input")(nil)
|
||||
|
||||
val1 := either.MonadFold(result1,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
val2 := either.MonadFold(result2,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
assert.Equal(t, "test", val1)
|
||||
assert.Equal(t, "test", val2)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_StringConcatenation tests string concatenation scenarios
|
||||
func TestApplicativeMonoid_StringConcatenation(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](strMonoid)
|
||||
|
||||
t.Run("build sentence", func(t *testing.T) {
|
||||
v1 := successValidator[string]("The")
|
||||
v2 := successValidator[string](" quick")
|
||||
v3 := successValidator[string](" brown")
|
||||
v4 := successValidator[string](" fox")
|
||||
|
||||
combined := m.Concat(
|
||||
m.Concat(
|
||||
m.Concat(v1, v2),
|
||||
v3,
|
||||
),
|
||||
v4,
|
||||
t.Run("with int addition monoid", func(t *testing.T) {
|
||||
intMonoid := MO.MakeMonoid(
|
||||
func(a, b int) int { return a + b },
|
||||
0,
|
||||
)
|
||||
m := AlternativeMonoid[string, int](intMonoid)
|
||||
|
||||
result := combined("input")(nil)
|
||||
t.Run("empty returns validator with zero", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
result := empty("input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of("The quick brown fox"), result)
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return -1 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 0, value)
|
||||
})
|
||||
|
||||
t.Run("concat combines decoded values when both succeed", func(t *testing.T) {
|
||||
validator1 := Of[string, int](10)
|
||||
validator2 := Of[string, int](32)
|
||||
|
||||
combined := m.Concat(validator1, validator2)
|
||||
result := combined("input")(nil)
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("concat uses fallback when first fails", func(t *testing.T) {
|
||||
failing := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "failed"},
|
||||
})
|
||||
}
|
||||
}
|
||||
succeeding := Of[string, int](42)
|
||||
|
||||
combined := m.Concat(failing, succeeding)
|
||||
result := combined("input")(nil)
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("multiple concat operations", func(t *testing.T) {
|
||||
validator1 := Of[string, int](1)
|
||||
validator2 := Of[string, int](2)
|
||||
validator3 := Of[string, int](3)
|
||||
validator4 := Of[string, int](4)
|
||||
|
||||
combined := m.Concat(m.Concat(m.Concat(validator1, validator2), validator3), validator4)
|
||||
result := combined("input")(nil)
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 10, value)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with empty strings", func(t *testing.T) {
|
||||
v1 := successValidator[string]("Hello")
|
||||
v2 := successValidator[string]("")
|
||||
v3 := successValidator[string]("World")
|
||||
t.Run("satisfies monoid laws", func(t *testing.T) {
|
||||
m := AlternativeMonoid[string, string](S.Monoid)
|
||||
|
||||
combined := m.Concat(m.Concat(v1, v2), v3)
|
||||
result := combined("input")(nil)
|
||||
validator1 := Of[string, string]("a")
|
||||
validator2 := Of[string, string]("b")
|
||||
validator3 := Of[string, string]("c")
|
||||
|
||||
assert.Equal(t, validation.Of("HelloWorld"), result)
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
result := m.Concat(m.Empty(), validator1)("input")(nil)
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "a", value)
|
||||
})
|
||||
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
result := m.Concat(validator1, m.Empty())("input")(nil)
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "a", value)
|
||||
})
|
||||
|
||||
t.Run("associativity", func(t *testing.T) {
|
||||
left := m.Concat(m.Concat(validator1, validator2), validator3)("input")(nil)
|
||||
right := m.Concat(validator1, m.Concat(validator2, validator3))("input")(nil)
|
||||
|
||||
leftVal := either.MonadFold(left,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
rightVal := either.MonadFold(right,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
assert.Equal(t, "abc", leftVal)
|
||||
assert.Equal(t, "abc", rightVal)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkApplicativeMonoid_ConcatSuccesses(b *testing.B) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
v1 := successValidator[string](5)
|
||||
v2 := successValidator[string](3)
|
||||
combined := m.Concat(v1, v2)
|
||||
// TestAltMonoid tests the AltMonoid function
|
||||
func TestAltMonoid(t *testing.T) {
|
||||
t.Run("with default value as zero", func(t *testing.T) {
|
||||
m := AltMonoid(func() Validate[string, int] {
|
||||
return Of[string, int](0)
|
||||
})
|
||||
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
_ = combined("test")(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkApplicativeMonoid_ConcatFailures(b *testing.B) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
v1 := failureValidator[string, int]("error 1")
|
||||
v2 := failureValidator[string, int]("error 2")
|
||||
combined := m.Concat(v1, v2)
|
||||
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
_ = combined("test")(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkApplicativeMonoid_MultipleConcat(b *testing.B) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
validators := make([]Validate[string, int], 10)
|
||||
for i := range validators {
|
||||
validators[i] = successValidator[string](i)
|
||||
}
|
||||
|
||||
// Chain all validators
|
||||
combined := validators[0]
|
||||
for i := 1; i < len(validators); i++ {
|
||||
combined = m.Concat(combined, validators[i])
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
_ = combined("test")(nil)
|
||||
}
|
||||
t.Run("empty returns the provided zero validator", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
result := empty("input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(0), result)
|
||||
})
|
||||
|
||||
t.Run("concat returns first validator when it succeeds", func(t *testing.T) {
|
||||
validator1 := Of[string, int](42)
|
||||
validator2 := Of[string, int](100)
|
||||
|
||||
combined := m.Concat(validator1, validator2)
|
||||
result := combined("input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("concat uses second as fallback when first fails", func(t *testing.T) {
|
||||
failing := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "failed"},
|
||||
})
|
||||
}
|
||||
}
|
||||
succeeding := Of[string, int](42)
|
||||
|
||||
combined := m.Concat(failing, succeeding)
|
||||
result := combined("input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("concat aggregates errors when both fail", func(t *testing.T) {
|
||||
failing1 := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 1"},
|
||||
})
|
||||
}
|
||||
}
|
||||
failing2 := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 2"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
combined := m.Concat(failing1, failing2)
|
||||
result := combined("input")(nil)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors from both validators")
|
||||
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "error 1")
|
||||
assert.Contains(t, messages, "error 2")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with failing zero", func(t *testing.T) {
|
||||
m := AltMonoid(func() Validate[string, int] {
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Messsage: "no default available"},
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty returns the failing zero validator", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
result := empty("input")(nil)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("concat with all failures aggregates errors", func(t *testing.T) {
|
||||
failing1 := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 1"},
|
||||
})
|
||||
}
|
||||
}
|
||||
failing2 := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 2"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
combined := m.Concat(failing1, failing2)
|
||||
result := combined("input")(nil)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("chaining multiple fallbacks", func(t *testing.T) {
|
||||
m := AltMonoid(func() Validate[string, string] {
|
||||
return Of[string, string]("default")
|
||||
})
|
||||
|
||||
primary := func(input string) Reader[Context, Validation[string]] {
|
||||
return func(ctx Context) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "primary failed"},
|
||||
})
|
||||
}
|
||||
}
|
||||
secondary := func(input string) Reader[Context, Validation[string]] {
|
||||
return func(ctx Context) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "secondary failed"},
|
||||
})
|
||||
}
|
||||
}
|
||||
tertiary := Of[string, string]("tertiary value")
|
||||
|
||||
combined := m.Concat(m.Concat(primary, secondary), tertiary)
|
||||
result := combined("input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of("tertiary value"), result)
|
||||
})
|
||||
|
||||
t.Run("difference from AlternativeMonoid", func(t *testing.T) {
|
||||
// AltMonoid - first success wins
|
||||
altM := AltMonoid(func() Validate[string, int] {
|
||||
return Of[string, int](0)
|
||||
})
|
||||
|
||||
// AlternativeMonoid - combines successes
|
||||
altMonoid := AlternativeMonoid[string, int](N.MonoidSum[int]())
|
||||
|
||||
validator1 := Of[string, int](10)
|
||||
validator2 := Of[string, int](32)
|
||||
|
||||
// AltMonoid: returns first success (10)
|
||||
result1 := altM.Concat(validator1, validator2)("input")(nil)
|
||||
value1 := either.MonadFold(result1,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 10, value1, "AltMonoid returns first success")
|
||||
|
||||
// AlternativeMonoid: combines both successes (10 + 32 = 42)
|
||||
result2 := altMonoid.Concat(validator1, validator2)("input")(nil)
|
||||
value2 := either.MonadFold(result2,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value2, "AlternativeMonoid combines successes")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ package validate
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"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/validation"
|
||||
@@ -271,4 +272,6 @@ type (
|
||||
// lower := strings.ToLower // Endomorphism[string]
|
||||
// normalize := compose(trim, lower) // Endomorphism[string]
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
)
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/readert"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/decode"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
@@ -429,6 +430,145 @@ func ChainLeft[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
|
||||
)
|
||||
}
|
||||
|
||||
// MonadChainLeft sequences a computation on the failure (Left) channel of a validation.
|
||||
//
|
||||
// This is the direct application version of ChainLeft. It operates on the error path
|
||||
// of validation, allowing you to transform, enrich, or recover from validation failures.
|
||||
// It's the dual of Chain - while Chain operates on success values, MonadChainLeft
|
||||
// operates on error values.
|
||||
//
|
||||
// # Key Behavior
|
||||
//
|
||||
// **Critical difference from standard Either operations**: This validation-specific
|
||||
// implementation **aggregates errors** using the Errors monoid. When the transformation
|
||||
// function returns a failure, both the original errors AND the new errors are combined,
|
||||
// ensuring comprehensive error reporting.
|
||||
//
|
||||
// 1. **Success Pass-Through**: If validation succeeds, the handler is never called and
|
||||
// the success value passes through unchanged.
|
||||
//
|
||||
// 2. **Error Recovery**: The handler can recover from failures by returning a successful
|
||||
// validation, converting Left to Right.
|
||||
//
|
||||
// 3. **Error Aggregation**: When the handler also returns a failure, both the original
|
||||
// errors and the new errors are combined using the Errors monoid.
|
||||
//
|
||||
// 4. **Input Access**: The handler returns a Validate[I, A] function, giving it access
|
||||
// to the original input value I for context-aware error handling.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - I: The input type
|
||||
// - A: The type of the validation result
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - fa: The Validate[I, A] to transform
|
||||
// - f: A Kleisli arrow that takes Errors and returns a Validate[I, A]. This function
|
||||
// is called only when validation fails, receiving the accumulated errors.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A Validate[I, A] that handles error cases according to the provided function.
|
||||
//
|
||||
// # Example: Error Recovery
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/optics/codec/validate"
|
||||
// "github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
// )
|
||||
//
|
||||
// // Validator that may fail
|
||||
// validatePositive := func(n int) validate.Reader[validation.Context, validation.Validation[int]] {
|
||||
// return func(ctx validation.Context) validation.Validation[int] {
|
||||
// if n > 0 {
|
||||
// return validation.Success(n)
|
||||
// }
|
||||
// return validation.FailureWithMessage[int](n, "must be positive")(ctx)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Recover from specific errors with a default value
|
||||
// withDefault := func(errs validation.Errors) validate.Validate[int, int] {
|
||||
// for _, err := range errs {
|
||||
// if err.Messsage == "must be positive" {
|
||||
// return validate.Of[int](0) // recover with default
|
||||
// }
|
||||
// }
|
||||
// // Propagate other errors
|
||||
// return func(input int) validate.Reader[validation.Context, validation.Validation[int]] {
|
||||
// return func(ctx validation.Context) validation.Validation[int] {
|
||||
// return either.Left[int](errs)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// validator := validate.MonadChainLeft(validatePositive, withDefault)
|
||||
// result := validator(-5)(nil)
|
||||
// // Result: Success(0) - recovered from failure
|
||||
//
|
||||
// # Example: Error Context Addition
|
||||
//
|
||||
// // Add contextual information to errors
|
||||
// addContext := func(errs validation.Errors) validate.Validate[string, int] {
|
||||
// return func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
|
||||
// return func(ctx validation.Context) validation.Validation[int] {
|
||||
// // Add context error (will be aggregated with original)
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {
|
||||
// Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
|
||||
// Messsage: "failed to validate user age",
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// validator := validate.MonadChainLeft(someValidator, addContext)
|
||||
// // Errors will include both original error and context
|
||||
//
|
||||
// # Example: Input-Dependent Recovery
|
||||
//
|
||||
// // Recover with different defaults based on input
|
||||
// smartDefault := func(errs validation.Errors) validate.Validate[string, int] {
|
||||
// return func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
|
||||
// return func(ctx validation.Context) validation.Validation[int] {
|
||||
// // Use input to determine appropriate default
|
||||
// if strings.Contains(input, "http:") {
|
||||
// return validation.Success(80)
|
||||
// }
|
||||
// if strings.Contains(input, "https:") {
|
||||
// return validation.Success(443)
|
||||
// }
|
||||
// return validation.Success(8080)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// validator := validate.MonadChainLeft(parsePort, smartDefault)
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - Errors are accumulated, not replaced - this ensures no validation failures are lost
|
||||
// - The handler has access to both the errors and the original input
|
||||
// - Success values bypass the handler completely
|
||||
// - This is the direct application version of ChainLeft
|
||||
// - This enables sophisticated error handling strategies including recovery, enrichment, and transformation
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - ChainLeft: The curried, point-free version
|
||||
// - OrElse: Semantic alias for ChainLeft emphasizing fallback logic
|
||||
// - MonadAlt: Simplified alternative that ignores error details
|
||||
// - Alt: Curried version of MonadAlt
|
||||
func MonadChainLeft[I, A any](fa Validate[I, A], f Kleisli[I, Errors, A]) Validate[I, A] {
|
||||
return readert.MonadChain(
|
||||
decode.MonadChainLeft,
|
||||
fa,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// OrElse provides an alternative validation when the primary validation fails.
|
||||
//
|
||||
// This is a semantic alias for ChainLeft with identical behavior. The name "OrElse"
|
||||
@@ -628,3 +768,218 @@ func Ap[B, I, A any](fa Validate[I, A]) Operator[I, func(A) B, B] {
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// Alt provides an alternative validator when the primary validator fails.
|
||||
//
|
||||
// This is the curried, point-free version of MonadAlt. It creates an operator that
|
||||
// transforms a validator by adding a fallback alternative. When the first validator
|
||||
// fails, the second (lazily evaluated) validator is tried. If both fail, errors are
|
||||
// aggregated.
|
||||
//
|
||||
// Alt implements the Alternative typeclass pattern, providing a way to express
|
||||
// "try this, or else try that" logic in a composable way.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - I: The input type
|
||||
// - A: The type of the validation result
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - second: A lazy Validate[I, A] that serves as the fallback. It's only evaluated
|
||||
// if the first validator fails.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// An Operator[I, A, A] that transforms validators by adding alternative fallback logic.
|
||||
//
|
||||
// # Behavior
|
||||
//
|
||||
// - **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
|
||||
//
|
||||
// # Example: Fallback Validation
|
||||
//
|
||||
// import (
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// "github.com/IBM/fp-go/v2/optics/codec/validate"
|
||||
// "github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
// )
|
||||
//
|
||||
// // Primary validator that may fail
|
||||
// validateFromConfig := func(key string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// // Try to get value from config
|
||||
// if value, ok := config[key]; ok {
|
||||
// return validation.Success(value)
|
||||
// }
|
||||
// return validation.FailureWithMessage[string](key, "not in config")(ctx)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Fallback to environment variable
|
||||
// validateFromEnv := func(key string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// if value := os.Getenv(key); value != "" {
|
||||
// return validation.Success(value)
|
||||
// }
|
||||
// return validation.FailureWithMessage[string](key, "not in env")(ctx)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Use Alt to add fallback - point-free style
|
||||
// withFallback := validate.Alt(func() validate.Validate[string, string] {
|
||||
// return validateFromEnv
|
||||
// })
|
||||
//
|
||||
// validator := withFallback(validateFromConfig)
|
||||
// result := validator("DATABASE_URL")(nil)
|
||||
// // Tries config first, falls back to environment variable
|
||||
//
|
||||
// # Example: Pipeline with Multiple Alternatives
|
||||
//
|
||||
// // Chain multiple alternatives using function composition
|
||||
// validator := F.Pipe2(
|
||||
// validateFromDatabase,
|
||||
// validate.Alt(func() validate.Validate[string, Config] {
|
||||
// return validateFromCache
|
||||
// }),
|
||||
// validate.Alt(func() validate.Validate[string, Config] {
|
||||
// return validate.Of[string](defaultConfig)
|
||||
// }),
|
||||
// )
|
||||
// // Tries database, then cache, then default
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - The second validator is lazily evaluated for efficiency
|
||||
// - First success short-circuits evaluation
|
||||
// - Errors are aggregated when both fail
|
||||
// - This is the point-free version of MonadAlt
|
||||
// - Useful for building validation pipelines with F.Pipe
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - MonadAlt: The direct application version
|
||||
// - ChainLeft: The more general error transformation operator
|
||||
// - OrElse: Semantic alias for ChainLeft
|
||||
// - AltMonoid: For combining multiple alternatives with monoid structure
|
||||
func Alt[I, A any](second Lazy[Validate[I, A]]) Operator[I, A, A] {
|
||||
return ChainLeft(function.Ignore1of1[Errors](second))
|
||||
}
|
||||
|
||||
// MonadAlt provides an alternative validator when the primary validator fails.
|
||||
//
|
||||
// This is the direct application version of Alt. It takes two validators and returns
|
||||
// a new validator that tries the first, and if it fails, tries the second. If both
|
||||
// fail, errors from both are aggregated.
|
||||
//
|
||||
// MonadAlt implements the Alternative typeclass pattern, enabling "try this, or else
|
||||
// try that" logic with comprehensive error reporting.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - I: The input type
|
||||
// - A: The type of the validation result
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - first: The primary Validate[I, A] to try first
|
||||
// - second: A lazy Validate[I, A] that serves as the fallback. It's only evaluated
|
||||
// if the first validator fails.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A Validate[I, A] that tries the first validator, falling back to the second if needed.
|
||||
//
|
||||
// # Behavior
|
||||
//
|
||||
// - **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
|
||||
//
|
||||
// # Example: Configuration with Fallback
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/optics/codec/validate"
|
||||
// "github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
// )
|
||||
//
|
||||
// // Primary validator
|
||||
// validateFromConfig := func(key string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// if value, ok := config[key]; ok {
|
||||
// return validation.Success(value)
|
||||
// }
|
||||
// return validation.FailureWithMessage[string](key, "not in config")(ctx)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Fallback validator
|
||||
// validateFromEnv := func(key string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// if value := os.Getenv(key); value != "" {
|
||||
// return validation.Success(value)
|
||||
// }
|
||||
// return validation.FailureWithMessage[string](key, "not in env")(ctx)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Combine with MonadAlt
|
||||
// validator := validate.MonadAlt(
|
||||
// validateFromConfig,
|
||||
// func() validate.Validate[string, string] { return validateFromEnv },
|
||||
// )
|
||||
// result := validator("DATABASE_URL")(nil)
|
||||
// // Tries config first, falls back to environment variable
|
||||
//
|
||||
// # Example: Multiple Fallbacks
|
||||
//
|
||||
// // Chain multiple alternatives
|
||||
// validator := validate.MonadAlt(
|
||||
// validate.MonadAlt(
|
||||
// validateFromDatabase,
|
||||
// func() validate.Validate[string, Config] { return validateFromCache },
|
||||
// ),
|
||||
// func() validate.Validate[string, Config] { return validate.Of[string](defaultConfig) },
|
||||
// )
|
||||
// // Tries database, then cache, then default
|
||||
//
|
||||
// # Example: Error Aggregation
|
||||
//
|
||||
// failing1 := func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
|
||||
// return func(ctx validation.Context) validation.Validation[int] {
|
||||
// return validation.FailureWithMessage[int](input, "error 1")(ctx)
|
||||
// }
|
||||
// }
|
||||
// failing2 := func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
|
||||
// return func(ctx validation.Context) validation.Validation[int] {
|
||||
// return validation.FailureWithMessage[int](input, "error 2")(ctx)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// validator := validate.MonadAlt(
|
||||
// failing1,
|
||||
// func() validate.Validate[string, int] { return failing2 },
|
||||
// )
|
||||
// result := validator("input")(nil)
|
||||
// // result contains both "error 1" and "error 2"
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - The second validator is lazily evaluated for efficiency
|
||||
// - First success short-circuits evaluation (second not called)
|
||||
// - Errors are aggregated when both fail
|
||||
// - This is equivalent to Alt but with direct application
|
||||
// - Both validators receive the same input value
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Alt: The curried, point-free version
|
||||
// - MonadChainLeft: The underlying error transformation operation
|
||||
// - OrElse: Semantic alias for ChainLeft
|
||||
// - AltMonoid: For combining multiple alternatives with monoid structure
|
||||
func MonadAlt[I, A any](first Validate[I, A], second Lazy[Validate[I, A]]) Validate[I, A] {
|
||||
return MonadChainLeft(first, function.Ignore1of1[Errors](second))
|
||||
}
|
||||
|
||||
@@ -474,3 +474,168 @@ func Applicative[A, B any]() applicative.Applicative[A, B, Validation[A], Valida
|
||||
func OrElse[A any](f Kleisli[Errors, A]) Operator[A, A] {
|
||||
return ChainLeft(f)
|
||||
}
|
||||
|
||||
// MonadAlt implements the Alternative operation for Validation, providing fallback behavior.
|
||||
// If the first validation fails, it evaluates and returns the second validation as an alternative.
|
||||
// If the first validation succeeds, it returns the first validation without evaluating the second.
|
||||
//
|
||||
// This is the fundamental operation for the Alt typeclass, enabling "try first, fallback to second"
|
||||
// semantics. It's particularly useful for:
|
||||
// - Providing default values when validation fails
|
||||
// - Trying multiple validation strategies in sequence
|
||||
// - Building validation pipelines with fallback logic
|
||||
// - Implementing optional validation with defaults
|
||||
//
|
||||
// **Key behavior**: When both validations fail, MonadAlt DOES accumulate errors from both
|
||||
// validations using the Errors monoid. This is different from standard Either Alt behavior.
|
||||
// The error accumulation happens through the underlying ChainLeft/chainErrors mechanism.
|
||||
//
|
||||
// The second parameter is lazy (Lazy[Validation[A]]) to avoid unnecessary computation when
|
||||
// the first validation succeeds. The second validation is only evaluated if needed.
|
||||
//
|
||||
// Behavior:
|
||||
// - First succeeds: returns first validation (second is not evaluated)
|
||||
// - First fails, second succeeds: returns second validation
|
||||
// - Both fail: aggregates errors from both validations
|
||||
//
|
||||
// This is useful for:
|
||||
// - Fallback values: provide defaults when primary validation fails
|
||||
// - Alternative strategies: try different validation approaches
|
||||
// - Optional validation: make validation optional with a default
|
||||
// - Chaining attempts: try multiple sources until one succeeds
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the successful value
|
||||
//
|
||||
// Parameters:
|
||||
// - first: The primary validation to try
|
||||
// - second: A lazy computation producing the fallback validation (only evaluated if first fails)
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// The first validation if it succeeds, otherwise the second validation
|
||||
//
|
||||
// Example - Fallback to default:
|
||||
//
|
||||
// primary := parseConfig("config.json") // Fails
|
||||
// fallback := func() Validation[Config] {
|
||||
// return Success(defaultConfig)
|
||||
// }
|
||||
// result := MonadAlt(primary, fallback)
|
||||
// // Result: Success(defaultConfig)
|
||||
//
|
||||
// Example - First succeeds (second not evaluated):
|
||||
//
|
||||
// primary := Success(42)
|
||||
// fallback := func() Validation[int] {
|
||||
// panic("never called") // This won't execute
|
||||
// }
|
||||
// result := MonadAlt(primary, fallback)
|
||||
// // Result: Success(42)
|
||||
//
|
||||
// Example - Chaining multiple alternatives:
|
||||
//
|
||||
// result := MonadAlt(
|
||||
// parseFromEnv("API_KEY"),
|
||||
// func() Validation[string] {
|
||||
// return MonadAlt(
|
||||
// parseFromFile(".env"),
|
||||
// func() Validation[string] {
|
||||
// return Success("default-key")
|
||||
// },
|
||||
// )
|
||||
// },
|
||||
// )
|
||||
// // Tries: env var → file → default (uses first that succeeds)
|
||||
//
|
||||
// Example - Error accumulation when both fail:
|
||||
//
|
||||
// v1 := Failures[int](Errors{
|
||||
// &ValidationError{Messsage: "error 1"},
|
||||
// &ValidationError{Messsage: "error 2"},
|
||||
// })
|
||||
// v2 := func() Validation[int] {
|
||||
// return Failures[int](Errors{
|
||||
// &ValidationError{Messsage: "error 3"},
|
||||
// })
|
||||
// }
|
||||
// result := MonadAlt(v1, v2)
|
||||
// // Result: Failures with ALL errors ["error 1", "error 2", "error 3"]
|
||||
// // The errors from v1 are aggregated with errors from v2
|
||||
func MonadAlt[A any](first Validation[A], second Lazy[Validation[A]]) Validation[A] {
|
||||
return MonadChainLeft(first, function.Ignore1of1[Errors](second))
|
||||
}
|
||||
|
||||
// Alt is the curried version of [MonadAlt].
|
||||
// Returns a function that provides fallback behavior for a Validation.
|
||||
//
|
||||
// This is useful for creating reusable fallback operators that can be applied
|
||||
// to multiple validations, or for use in function composition pipelines.
|
||||
//
|
||||
// The returned function takes a validation and returns either that validation
|
||||
// (if successful) or the provided alternative (if the validation fails).
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the successful value
|
||||
//
|
||||
// Parameters:
|
||||
// - second: A lazy computation producing the fallback validation
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// A function that takes a Validation[A] and returns a Validation[A] with fallback behavior
|
||||
//
|
||||
// Example - Creating a reusable fallback operator:
|
||||
//
|
||||
// withDefault := Alt(func() Validation[int] {
|
||||
// return Success(0)
|
||||
// })
|
||||
//
|
||||
// result1 := withDefault(parseNumber("42")) // Success(42)
|
||||
// result2 := withDefault(parseNumber("abc")) // Success(0) - fallback
|
||||
// result3 := withDefault(parseNumber("123")) // Success(123)
|
||||
//
|
||||
// Example - Using in a pipeline:
|
||||
//
|
||||
// import F "github.com/IBM/fp-go/v2/function"
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// parseFromEnv("CONFIG_PATH"),
|
||||
// Alt(func() Validation[string] {
|
||||
// return parseFromFile("config.json")
|
||||
// }),
|
||||
// Alt(func() Validation[string] {
|
||||
// return Success("./default-config.json")
|
||||
// }),
|
||||
// )
|
||||
// // Tries: env var → file → default path
|
||||
//
|
||||
// Example - Combining with Map:
|
||||
//
|
||||
// import F "github.com/IBM/fp-go/v2/function"
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// validatePositive(-5), // Fails
|
||||
// Alt(func() Validation[int] { return Success(1) }),
|
||||
// Map(func(x int) int { return x * 2 }),
|
||||
// )
|
||||
// // Result: Success(2) - uses fallback value 1, then doubles it
|
||||
//
|
||||
// Example - Multiple fallback layers:
|
||||
//
|
||||
// primaryFallback := Alt(func() Validation[Config] {
|
||||
// return loadFromFile("backup.json")
|
||||
// })
|
||||
// secondaryFallback := Alt(func() Validation[Config] {
|
||||
// return Success(defaultConfig)
|
||||
// })
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// loadFromFile("config.json"),
|
||||
// primaryFallback,
|
||||
// secondaryFallback,
|
||||
// )
|
||||
// // Tries: config.json → backup.json → default
|
||||
func Alt[A any](second Lazy[Validation[A]]) Operator[A, A] {
|
||||
return ChainLeft(function.Ignore1of1[Errors](second))
|
||||
}
|
||||
|
||||
@@ -52,3 +52,177 @@ func ApplicativeMonoid[A any](m Monoid[A]) Monoid[Validation[A]] {
|
||||
m,
|
||||
)
|
||||
}
|
||||
|
||||
// AlternativeMonoid creates a Monoid instance for Validation[A] using the Alternative pattern.
|
||||
// This combines the applicative error-accumulation behavior with the alternative fallback behavior,
|
||||
// allowing you to both accumulate errors and provide fallback alternatives.
|
||||
//
|
||||
// The Alternative pattern provides two key operations:
|
||||
// - Applicative operations (Of, Map, Ap): accumulate errors when combining validations
|
||||
// - Alternative operation (Alt): provide fallback when a validation fails
|
||||
//
|
||||
// This monoid is particularly useful when you want to:
|
||||
// - Try multiple validation strategies and fall back to alternatives
|
||||
// - Combine successful values using the provided monoid
|
||||
// - Accumulate all errors from failed attempts
|
||||
// - Build validation pipelines with fallback logic
|
||||
//
|
||||
// The resulting monoid:
|
||||
// - Empty: Returns a successful validation with the empty value from the inner monoid
|
||||
// - Concat: Combines two validations using both applicative and alternative semantics:
|
||||
// - If first succeeds and second succeeds: combines values using inner monoid
|
||||
// - If first fails: tries second as fallback (alternative behavior)
|
||||
// - If both fail: accumulates all errors
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the successful value
|
||||
//
|
||||
// Parameters:
|
||||
// - m: The monoid for combining successful values of type A
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// A Monoid[Validation[A]] that combines applicative and alternative behaviors
|
||||
//
|
||||
// Example - Combining successful validations:
|
||||
//
|
||||
// import "github.com/IBM/fp-go/v2/string"
|
||||
//
|
||||
// m := AlternativeMonoid(string.Monoid)
|
||||
// v1 := Success("Hello")
|
||||
// v2 := Success(" World")
|
||||
// result := m.Concat(v1, v2)
|
||||
// // Result: Success("Hello World")
|
||||
//
|
||||
// Example - Fallback behavior:
|
||||
//
|
||||
// m := AlternativeMonoid(string.Monoid)
|
||||
// v1 := Failures[string](Errors{&ValidationError{Messsage: "first failed"}})
|
||||
// v2 := Success("fallback value")
|
||||
// result := m.Concat(v1, v2)
|
||||
// // Result: Success("fallback value") - second validation used as fallback
|
||||
//
|
||||
// Example - Error accumulation when both fail:
|
||||
//
|
||||
// m := AlternativeMonoid(string.Monoid)
|
||||
// v1 := Failures[string](Errors{&ValidationError{Messsage: "error 1"}})
|
||||
// v2 := Failures[string](Errors{&ValidationError{Messsage: "error 2"}})
|
||||
// result := m.Concat(v1, v2)
|
||||
// // Result: Failures with accumulated errors: ["error 1", "error 2"]
|
||||
//
|
||||
// Example - Building validation with fallbacks:
|
||||
//
|
||||
// import N "github.com/IBM/fp-go/v2/number"
|
||||
//
|
||||
// m := AlternativeMonoid(N.MonoidSum[int]())
|
||||
//
|
||||
// // Try to parse from different sources
|
||||
// fromEnv := parseFromEnv() // Fails
|
||||
// fromConfig := parseFromConfig() // Succeeds with 42
|
||||
// fromDefault := Success(0) // Default fallback
|
||||
//
|
||||
// result := m.Concat(m.Concat(fromEnv, fromConfig), fromDefault)
|
||||
// // Result: Success(42) - uses first successful validation
|
||||
func AlternativeMonoid[A any](m Monoid[A]) Monoid[Validation[A]] {
|
||||
return M.AlternativeMonoid(
|
||||
Of[A],
|
||||
MonadMap[A, func(A) A],
|
||||
MonadAp[A, A],
|
||||
MonadAlt[A],
|
||||
m,
|
||||
)
|
||||
}
|
||||
|
||||
// AltMonoid creates a Monoid instance for Validation[A] using the Alt (alternative) operation.
|
||||
// This monoid provides a way to combine validations with fallback behavior, where the second
|
||||
// validation is used as an alternative if the first one fails.
|
||||
//
|
||||
// The Alt operation implements the "try first, fallback to second" pattern, which is useful
|
||||
// for validation scenarios where you want to attempt multiple validation strategies in sequence
|
||||
// and use the first one that succeeds.
|
||||
//
|
||||
// The resulting monoid:
|
||||
// - Empty: Returns the provided zero value (a lazy computation that produces a Validation[A])
|
||||
// - Concat: Combines two validations using Alt semantics:
|
||||
// - If first succeeds: returns the first validation (ignores second)
|
||||
// - If first fails: returns the second validation as fallback
|
||||
//
|
||||
// This is different from [AlternativeMonoid] in that:
|
||||
// - AltMonoid uses a custom zero value (provided by the user)
|
||||
// - AlternativeMonoid derives the zero from an inner monoid
|
||||
// - AltMonoid is simpler and only provides fallback behavior
|
||||
// - AlternativeMonoid combines applicative and alternative behaviors
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the successful value
|
||||
//
|
||||
// Parameters:
|
||||
// - zero: A lazy computation that produces the identity/empty Validation[A].
|
||||
// This is typically a successful validation with a default value, or could be
|
||||
// a failure representing "no validation attempted"
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// A Monoid[Validation[A]] that combines validations with fallback behavior
|
||||
//
|
||||
// Example - Using default value as zero:
|
||||
//
|
||||
// m := AltMonoid(func() Validation[int] { return Success(0) })
|
||||
//
|
||||
// v1 := Failures[int](Errors{&ValidationError{Messsage: "failed"}})
|
||||
// v2 := Success(42)
|
||||
//
|
||||
// result := m.Concat(v1, v2)
|
||||
// // Result: Success(42) - falls back to second validation
|
||||
//
|
||||
// empty := m.Empty()
|
||||
// // Result: Success(0) - the provided zero value
|
||||
//
|
||||
// Example - Chaining multiple fallbacks:
|
||||
//
|
||||
// m := AltMonoid(func() Validation[string] {
|
||||
// return Success("default")
|
||||
// })
|
||||
//
|
||||
// primary := parseFromPrimarySource() // Fails
|
||||
// secondary := parseFromSecondary() // Fails
|
||||
// tertiary := parseFromTertiary() // Succeeds with "value"
|
||||
//
|
||||
// result := m.Concat(m.Concat(primary, secondary), tertiary)
|
||||
// // Result: Success("value") - uses first successful validation
|
||||
//
|
||||
// Example - All validations fail:
|
||||
//
|
||||
// m := AltMonoid(func() Validation[int] {
|
||||
// return Failures[int](Errors{&ValidationError{Messsage: "no default"}})
|
||||
// })
|
||||
//
|
||||
// v1 := Failures[int](Errors{&ValidationError{Messsage: "error 1"}})
|
||||
// v2 := Failures[int](Errors{&ValidationError{Messsage: "error 2"}})
|
||||
//
|
||||
// result := m.Concat(v1, v2)
|
||||
// // Result: Failures with errors from v2: ["error 2"]
|
||||
// // Note: Unlike AlternativeMonoid, errors are NOT accumulated
|
||||
//
|
||||
// Example - Building a validation pipeline with fallbacks:
|
||||
//
|
||||
// m := AltMonoid(func() Validation[Config] {
|
||||
// return Success(defaultConfig)
|
||||
// })
|
||||
//
|
||||
// // Try multiple configuration sources in order
|
||||
// configs := []Validation[Config]{
|
||||
// loadFromFile("config.json"), // Try file first
|
||||
// loadFromEnv(), // Then environment
|
||||
// loadFromRemote("api.example.com"), // Then remote API
|
||||
// }
|
||||
//
|
||||
// // Fold using the monoid to get first successful config
|
||||
// result := A.MonoidFold(m)(configs)
|
||||
// // Result: First successful config, or defaultConfig if all fail
|
||||
func AltMonoid[A any](zero Lazy[Validation[A]]) Monoid[Validation[A]] {
|
||||
return M.AltMonoid(
|
||||
zero,
|
||||
MonadAlt[A],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -131,17 +131,8 @@ func TestApplicativeMonoid(t *testing.T) {
|
||||
result1 := m.Concat(v, empty)
|
||||
result2 := m.Concat(empty, v)
|
||||
|
||||
val1 := either.MonadFold(result1,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
val2 := either.MonadFold(result2,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
assert.Equal(t, "test", val1)
|
||||
assert.Equal(t, "test", val2)
|
||||
assert.Equal(t, Of("test"), result1)
|
||||
assert.Equal(t, Of("test"), result2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -156,11 +147,7 @@ func TestApplicativeMonoid(t *testing.T) {
|
||||
t.Run("empty returns zero", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
|
||||
value := either.MonadFold(empty,
|
||||
func(Errors) int { return -1 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 0, value)
|
||||
assert.Equal(t, Of(0), empty)
|
||||
})
|
||||
|
||||
t.Run("concat adds values", func(t *testing.T) {
|
||||
@@ -169,11 +156,7 @@ func TestApplicativeMonoid(t *testing.T) {
|
||||
|
||||
result := m.Concat(v1, v2)
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("multiple concat operations", func(t *testing.T) {
|
||||
@@ -184,11 +167,7 @@ func TestApplicativeMonoid(t *testing.T) {
|
||||
|
||||
result := m.Concat(m.Concat(m.Concat(v1, v2), v3), v4)
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 10, value)
|
||||
assert.Equal(t, Of(10), result)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -235,21 +214,13 @@ func TestMonoidLaws(t *testing.T) {
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
// empty + a = a
|
||||
result := m.Concat(m.Empty(), v1)
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "a", value)
|
||||
assert.Equal(t, Of("a"), result)
|
||||
})
|
||||
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
// a + empty = a
|
||||
result := m.Concat(v1, m.Empty())
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "a", value)
|
||||
assert.Equal(t, Of("a"), result)
|
||||
})
|
||||
|
||||
t.Run("associativity", func(t *testing.T) {
|
||||
@@ -258,17 +229,8 @@ func TestMonoidLaws(t *testing.T) {
|
||||
left := m.Concat(m.Concat(v1, v2), v3)
|
||||
right := m.Concat(v1, m.Concat(v2, v3))
|
||||
|
||||
leftVal := either.MonadFold(left,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
rightVal := either.MonadFold(right,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
assert.Equal(t, "abc", leftVal)
|
||||
assert.Equal(t, "abc", rightVal)
|
||||
assert.Equal(t, Of("abc"), left)
|
||||
assert.Equal(t, Of("abc"), right)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -322,11 +284,7 @@ func TestApplicativeMonoidEdgeCases(t *testing.T) {
|
||||
|
||||
result := m.Concat(v1, v2)
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) Counter { return Counter{} },
|
||||
F.Identity[Counter],
|
||||
)
|
||||
assert.Equal(t, 15, value.Count)
|
||||
assert.Equal(t, Of(Counter{Count: 15}), result)
|
||||
})
|
||||
|
||||
t.Run("empty concat empty", func(t *testing.T) {
|
||||
@@ -334,10 +292,6 @@ func TestApplicativeMonoidEdgeCases(t *testing.T) {
|
||||
|
||||
result := m.Concat(m.Empty(), m.Empty())
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "ERROR" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "", value)
|
||||
assert.Equal(t, Of(""), result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package validation
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
@@ -257,4 +258,6 @@ type (
|
||||
// double := func(x int) int { return x * 2 } // Endomorphism[int]
|
||||
// result := LetL(lens, double)(Success(21)) // Success(42)
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
)
|
||||
|
||||
153
v2/optics/iso/prism/compose.go
Normal file
153
v2/optics/iso/prism/compose.go
Normal 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),
|
||||
)
|
||||
}
|
||||
}
|
||||
435
v2/optics/iso/prism/compose_test.go
Normal file
435
v2/optics/iso/prism/compose_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
99
v2/optics/iso/prism/types.go
Normal file
99
v2/optics/iso/prism/types.go
Normal 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]
|
||||
)
|
||||
156
v2/optics/prism/iso/compose.go
Normal file
156
v2/optics/prism/iso/compose.go
Normal 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),
|
||||
)
|
||||
}
|
||||
}
|
||||
369
v2/optics/prism/iso/compose_test.go
Normal file
369
v2/optics/prism/iso/compose_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
112
v2/optics/prism/iso/types.go
Normal file
112
v2/optics/prism/iso/types.go
Normal 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]
|
||||
)
|
||||
@@ -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",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
360
v2/readerioresult/consumer_test.go
Normal file
360
v2/readerioresult/consumer_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
262
v2/result/filterable.go
Normal 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)
|
||||
}
|
||||
689
v2/result/filterable_test.go
Normal file
689
v2/result/filterable_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user