mirror of
https://github.com/IBM/fp-go.git
synced 2026-01-01 00:10:32 +02:00
Compare commits
7 Commits
main
...
cleue-or-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2329edea36 | ||
|
|
5b910a39af | ||
|
|
5ba6bd9583 | ||
|
|
d6df9ab738 | ||
|
|
f139aab2b8 | ||
|
|
638c6357da | ||
|
|
451cbc8bf6 |
@@ -55,7 +55,7 @@ import (
|
||||
|
||||
// Create a pipeline of transformations
|
||||
pipeline := F.Flow3(
|
||||
A.Filter(func(x int) bool { return x > 0 }), // Keep positive numbers
|
||||
A.Filter(N.MoreThan(0)), // Keep positive numbers
|
||||
A.Map(N.Mul(2)), // Double each number
|
||||
A.Reduce(func(acc, x int) int { return acc + x }, 0), // Sum them up
|
||||
)
|
||||
|
||||
@@ -16,22 +16,11 @@
|
||||
package array
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/eq"
|
||||
)
|
||||
|
||||
func equals[T any](left, right []T, eq func(T, T) bool) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
for i, v1 := range left {
|
||||
v2 := right[i]
|
||||
if !eq(v1, v2) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Eq creates an equality checker for arrays given an equality checker for elements.
|
||||
// Two arrays are considered equal if they have the same length and all corresponding
|
||||
// elements are equal according to the provided Eq instance.
|
||||
@@ -46,6 +35,11 @@ func equals[T any](left, right []T, eq func(T, T) bool) bool {
|
||||
func Eq[T any](e E.Eq[T]) E.Eq[[]T] {
|
||||
eq := e.Equals
|
||||
return E.FromEquals(func(left, right []T) bool {
|
||||
return equals(left, right, eq)
|
||||
return slices.EqualFunc(left, right, eq)
|
||||
})
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func StrictEquals[T comparable]() E.Eq[[]T] {
|
||||
return E.FromEquals(slices.Equal[[]T])
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
// assert.ArrayNotEmpty(arr)(t)
|
||||
//
|
||||
// // Partial application - create reusable assertions
|
||||
// isPositive := assert.That(func(n int) bool { return n > 0 })
|
||||
// isPositive := assert.That(N.MoreThan(0))
|
||||
// // Later, apply to different values:
|
||||
// isPositive(42)(t) // Passes
|
||||
// isPositive(-5)(t) // Fails
|
||||
@@ -416,7 +416,7 @@ func NotContainsKey[T any, K comparable](expected K) Kleisli[map[K]T] {
|
||||
//
|
||||
// func TestThat(t *testing.T) {
|
||||
// // Test if a number is positive
|
||||
// isPositive := func(n int) bool { return n > 0 }
|
||||
// isPositive := N.MoreThan(0)
|
||||
// assert.That(isPositive)(42)(t) // Passes
|
||||
// assert.That(isPositive)(-5)(t) // Fails
|
||||
//
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/assert"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
@@ -111,7 +112,7 @@ func Example_predicateAssertions() {
|
||||
var t *testing.T // placeholder for example
|
||||
|
||||
// Test if a number is positive
|
||||
isPositive := func(n int) bool { return n > 0 }
|
||||
isPositive := N.MoreThan(0)
|
||||
assert.That(isPositive)(42)(t)
|
||||
|
||||
// Test if a string is uppercase
|
||||
|
||||
@@ -6,6 +6,11 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
// IOResult represents a synchronous computation that may fail with an error.
|
||||
// It's an alias for ioresult.IOResult[T].
|
||||
IOResult[T any] = ioresult.IOResult[T]
|
||||
Result[T any] = result.Result[T]
|
||||
|
||||
// Result represents a computation that may fail with an error.
|
||||
// It's an alias for result.Result[T].
|
||||
Result[T any] = result.Result[T]
|
||||
)
|
||||
|
||||
@@ -44,11 +44,11 @@ var (
|
||||
)
|
||||
|
||||
// Close closes an object
|
||||
func Close[C io.Closer](c C) RIOE.ReaderIOResult[any] {
|
||||
func Close[C io.Closer](c C) RIOE.ReaderIOResult[struct{}] {
|
||||
return F.Pipe2(
|
||||
c,
|
||||
IOEF.Close[C],
|
||||
RIOE.FromIOEither[any],
|
||||
RIOE.FromIOEither[struct{}],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
51
v2/context/readerioresult/filter.go
Normal file
51
v2/context/readerioresult/filter.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// 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"
|
||||
|
||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||
)
|
||||
|
||||
// FilterOrElse filters a ReaderIOResult value based on a predicate.
|
||||
// This is a convenience wrapper around readerioresult.FilterOrElse that fixes
|
||||
// the context type to context.Context.
|
||||
//
|
||||
// If the predicate returns true for the Right value, it passes through unchanged.
|
||||
// If the predicate returns false, it transforms the Right value into a Left (error) using onFalse.
|
||||
// Left values are passed through unchanged.
|
||||
//
|
||||
// Parameters:
|
||||
// - pred: A predicate function that tests the Right value
|
||||
// - onFalse: A function that converts the failing value into an error
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that filters ReaderIOResult values based on the predicate
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Validate that a number is positive
|
||||
// isPositive := N.MoreThan(0)
|
||||
// onNegative := func(n int) error { return fmt.Errorf("%d is not positive", n) }
|
||||
//
|
||||
// filter := readerioresult.FilterOrElse(isPositive, onNegative)
|
||||
// result := filter(readerioresult.Right(42))(context.Background())()
|
||||
//
|
||||
//go:inline
|
||||
func FilterOrElse[A any](pred Predicate[A], onFalse func(A) error) Operator[A, A] {
|
||||
return RIOR.FilterOrElse[context.Context](pred, onFalse)
|
||||
}
|
||||
@@ -235,7 +235,7 @@ func TestApPar(t *testing.T) {
|
||||
func TestFromPredicate(t *testing.T) {
|
||||
t.Run("Predicate true", func(t *testing.T) {
|
||||
pred := FromPredicate(
|
||||
func(x int) bool { return x > 0 },
|
||||
N.MoreThan(0),
|
||||
func(x int) error { return fmt.Errorf("value %d is not positive", x) },
|
||||
)
|
||||
result := pred(5)
|
||||
@@ -244,7 +244,7 @@ func TestFromPredicate(t *testing.T) {
|
||||
|
||||
t.Run("Predicate false", func(t *testing.T) {
|
||||
pred := FromPredicate(
|
||||
func(x int) bool { return x > 0 },
|
||||
N.MoreThan(0),
|
||||
func(x int) error { return fmt.Errorf("value %d is not positive", x) },
|
||||
)
|
||||
result := pred(-5)
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readereither"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
@@ -140,4 +141,6 @@ type (
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
|
||||
Trampoline[B, L any] = tailrec.Trampoline[B, L]
|
||||
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
)
|
||||
|
||||
51
v2/context/readerresult/filter.go
Normal file
51
v2/context/readerresult/filter.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
RR "github.com/IBM/fp-go/v2/readerresult"
|
||||
)
|
||||
|
||||
// FilterOrElse filters a ReaderResult value based on a predicate.
|
||||
// This is a convenience wrapper around readerresult.FilterOrElse that fixes
|
||||
// the context type to context.Context.
|
||||
//
|
||||
// If the predicate returns true for the Right value, it passes through unchanged.
|
||||
// If the predicate returns false, it transforms the Right value into a Left (error) using onFalse.
|
||||
// Left values are passed through unchanged.
|
||||
//
|
||||
// Parameters:
|
||||
// - pred: A predicate function that tests the Right value
|
||||
// - onFalse: A function that converts the failing value into an error
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that filters ReaderResult values based on the predicate
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Validate that a number is positive
|
||||
// isPositive := N.MoreThan(0)
|
||||
// onNegative := func(n int) error { return fmt.Errorf("%d is not positive", n) }
|
||||
//
|
||||
// filter := readerresult.FilterOrElse(isPositive, onNegative)
|
||||
// result := filter(readerresult.Right(42))(context.Background())
|
||||
//
|
||||
//go:inline
|
||||
func FilterOrElse[A any](pred Predicate[A], onFalse func(A) error) Operator[A, A] {
|
||||
return RR.FilterOrElse[context.Context](pred, onFalse)
|
||||
}
|
||||
@@ -73,6 +73,48 @@ func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) Kleisli[A, A
|
||||
return readereither.FromPredicate[context.Context](pred, onFalse)
|
||||
}
|
||||
|
||||
// OrElse recovers from a Left (error) by providing an alternative computation with access to context.Context.
|
||||
// If the ReaderResult is Right, it returns the value unchanged.
|
||||
// If the ReaderResult is Left, it applies the provided function to the error value,
|
||||
// which returns a new ReaderResult that replaces the original.
|
||||
//
|
||||
// This is useful for error recovery, fallback logic, or chaining alternative computations
|
||||
// that need access to the context (for cancellation, deadlines, or values).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Recover with context-aware fallback
|
||||
// recover := readerresult.OrElse(func(err error) readerresult.ReaderResult[int] {
|
||||
// if err.Error() == "not found" {
|
||||
// return func(ctx context.Context) result.Result[int] {
|
||||
// // Could check ctx.Err() here
|
||||
// return result.Of(42)
|
||||
// }
|
||||
// }
|
||||
// return readerresult.Left[int](err)
|
||||
// })
|
||||
//
|
||||
// OrElse recovers from a Left (error) by providing an alternative computation.
|
||||
// If the ReaderResult is Right, it returns the value unchanged.
|
||||
// If the ReaderResult is Left, it applies the provided function to the error value,
|
||||
// which returns a new ReaderResult that replaces the original.
|
||||
//
|
||||
// This is useful for error recovery, fallback logic, or chaining alternative computations
|
||||
// in the context of Reader computations with context.Context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Recover from specific errors with fallback values
|
||||
// recover := readerresult.OrElse(func(err error) readerresult.ReaderResult[int] {
|
||||
// if err.Error() == "not found" {
|
||||
// return readerresult.Of[int](0) // default value
|
||||
// }
|
||||
// return readerresult.Left[int](err) // propagate other errors
|
||||
// })
|
||||
// result := recover(readerresult.Left[int](errors.New("not found")))(ctx) // Right(0)
|
||||
// result := recover(readerresult.Of(42))(ctx) // Right(42) - unchanged
|
||||
//
|
||||
//go:inline
|
||||
func OrElse[A any](onLeft Kleisli[error, A]) Kleisli[ReaderResult[A], A] {
|
||||
return readereither.OrElse(F.Flow2(onLeft, WithContext))
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ package readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
@@ -313,3 +314,69 @@ func TestMonadChainTo(t *testing.T) {
|
||||
assert.False(t, secondExecuted, "second reader should not be executed on error")
|
||||
})
|
||||
}
|
||||
|
||||
func TestOrElse(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test OrElse with Right - should pass through unchanged
|
||||
t.Run("Right value unchanged", func(t *testing.T) {
|
||||
rightValue := Of(42)
|
||||
recover := OrElse(func(err error) ReaderResult[int] {
|
||||
return Left[int](errors.New("should not be called"))
|
||||
})
|
||||
res := recover(rightValue)(ctx)
|
||||
assert.Equal(t, E.Of[error](42), res)
|
||||
})
|
||||
|
||||
// Test OrElse with Left - should recover with fallback
|
||||
t.Run("Left value recovered", func(t *testing.T) {
|
||||
leftValue := Left[int](errors.New("not found"))
|
||||
recoverWithFallback := OrElse(func(err error) ReaderResult[int] {
|
||||
if err.Error() == "not found" {
|
||||
return func(ctx context.Context) E.Either[error, int] {
|
||||
return E.Of[error](99)
|
||||
}
|
||||
}
|
||||
return Left[int](err)
|
||||
})
|
||||
res := recoverWithFallback(leftValue)(ctx)
|
||||
assert.Equal(t, E.Of[error](99), res)
|
||||
})
|
||||
|
||||
// Test OrElse with Left - should propagate other errors
|
||||
t.Run("Left value propagated", func(t *testing.T) {
|
||||
leftValue := Left[int](errors.New("fatal error"))
|
||||
recoverWithFallback := OrElse(func(err error) ReaderResult[int] {
|
||||
if err.Error() == "not found" {
|
||||
return Of(99)
|
||||
}
|
||||
return Left[int](err)
|
||||
})
|
||||
res := recoverWithFallback(leftValue)(ctx)
|
||||
assert.True(t, E.IsLeft(res))
|
||||
val, err := E.UnwrapError(res)
|
||||
assert.Equal(t, 0, val)
|
||||
assert.Equal(t, "fatal error", err.Error())
|
||||
})
|
||||
|
||||
// Test OrElse with context-aware recovery
|
||||
t.Run("Context-aware recovery", func(t *testing.T) {
|
||||
type ctxKey string
|
||||
ctxWithValue := context.WithValue(ctx, ctxKey("fallback"), 123)
|
||||
|
||||
leftValue := Left[int](errors.New("use fallback"))
|
||||
ctxRecover := OrElse(func(err error) ReaderResult[int] {
|
||||
if err.Error() == "use fallback" {
|
||||
return func(ctx context.Context) E.Either[error, int] {
|
||||
if val := ctx.Value(ctxKey("fallback")); val != nil {
|
||||
return E.Of[error](val.(int))
|
||||
}
|
||||
return E.Left[int](errors.New("no fallback"))
|
||||
}
|
||||
}
|
||||
return Left[int](err)
|
||||
})
|
||||
res := ctxRecover(leftValue)(ctxWithValue)
|
||||
assert.Equal(t, E.Of[error](123), res)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readereither"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
@@ -68,4 +69,5 @@ type (
|
||||
Prism[S, T any] = prism.Prism[S, T]
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
Trampoline[A, B any] = tailrec.Trampoline[A, B]
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
)
|
||||
|
||||
55
v2/context/statereaderioresult/filter.go
Normal file
55
v2/context/statereaderioresult/filter.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2024 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package statereaderioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/statereaderioeither"
|
||||
)
|
||||
|
||||
// FilterOrElse filters a StateReaderIOResult value based on a predicate.
|
||||
// This is a convenience wrapper around statereaderioeither.FilterOrElse that fixes
|
||||
// the context type to context.Context and the error type to error.
|
||||
//
|
||||
// If the predicate returns true for the Right value, it passes through unchanged.
|
||||
// If the predicate returns false, it transforms the Right value into a Left (error) using onFalse.
|
||||
// Left values are passed through unchanged.
|
||||
//
|
||||
// Parameters:
|
||||
// - pred: A predicate function that tests the Right value
|
||||
// - onFalse: A function that converts the failing value into an error
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that filters StateReaderIOResult values based on the predicate
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type AppState struct {
|
||||
// Counter int
|
||||
// }
|
||||
//
|
||||
// // Validate that a number is positive
|
||||
// isPositive := N.MoreThan(0)
|
||||
// onNegative := func(n int) error { return fmt.Errorf("%d is not positive", n) }
|
||||
//
|
||||
// filter := statereaderioresult.FilterOrElse[AppState](isPositive, onNegative)
|
||||
// result := filter(statereaderioresult.Right[AppState](42))(AppState{})(context.Background())()
|
||||
//
|
||||
//go:inline
|
||||
func FilterOrElse[S, A any](pred Predicate[A], onFalse func(A) error) Operator[S, A, A] {
|
||||
return statereaderioeither.FilterOrElse[S, context.Context](pred, onFalse)
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/optics/iso/lens"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/IBM/fp-go/v2/state"
|
||||
@@ -81,4 +82,6 @@ type (
|
||||
// Operator represents a function that transforms one StateReaderIOResult into another.
|
||||
// This is commonly used for building composable operations via Map, Chain, etc.
|
||||
Operator[S, A, B any] = Reader[StateReaderIOResult[S, A], StateReaderIOResult[S, B]]
|
||||
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
)
|
||||
|
||||
@@ -394,12 +394,12 @@ func UnwrapError[A any](ma Either[error, A]) (A, error) {
|
||||
// Example:
|
||||
//
|
||||
// isPositive := either.FromPredicate(
|
||||
// func(x int) bool { return x > 0 },
|
||||
// N.MoreThan(0),
|
||||
// func(x int) error { return errors.New("not positive") },
|
||||
// )
|
||||
// result := isPositive(42) // Right(42)
|
||||
// result := isPositive(-1) // Left(error)
|
||||
func FromPredicate[E, A any](pred func(A) bool, onFalse func(A) E) func(A) Either[E, A] {
|
||||
func FromPredicate[E, A any](pred Predicate[A], onFalse func(A) E) Kleisli[E, A, A] {
|
||||
return func(a A) Either[E, A] {
|
||||
if pred(a) {
|
||||
return Right[E](a)
|
||||
@@ -416,7 +416,7 @@ func FromPredicate[E, A any](pred func(A) bool, onFalse func(A) E) func(A) Eithe
|
||||
// result := either.FromNillable[int](errors.New("nil"))(ptr) // Left(error)
|
||||
// val := 42
|
||||
// result := either.FromNillable[int](errors.New("nil"))(&val) // Right(&42)
|
||||
func FromNillable[A, E any](e E) func(*A) Either[E, *A] {
|
||||
func FromNillable[A, E any](e E) Kleisli[E, *A, *A] {
|
||||
return FromPredicate(F.IsNonNil[A], F.Constant1[*A](e))
|
||||
}
|
||||
|
||||
@@ -450,7 +450,7 @@ func Reduce[E, A, B any](f func(B, A) B, initial B) func(Either[E, A]) B {
|
||||
// return either.Right[string](99)
|
||||
// })
|
||||
// result := alternative(either.Left[int](errors.New("fail"))) // Right(99)
|
||||
func AltW[E, E1, A any](that Lazy[Either[E1, A]]) func(Either[E, A]) Either[E1, A] {
|
||||
func AltW[E, E1, A any](that Lazy[Either[E1, A]]) Kleisli[E1, Either[E, A], A] {
|
||||
return Fold(F.Ignore1of1[E](that), Right[E1, A])
|
||||
}
|
||||
|
||||
@@ -466,16 +466,29 @@ func Alt[E, A any](that Lazy[Either[E, A]]) Operator[E, A, A] {
|
||||
return AltW[E](that)
|
||||
}
|
||||
|
||||
// OrElse recovers from a Left by providing an alternative computation.
|
||||
// OrElse recovers from a Left (error) by providing an alternative computation.
|
||||
// If the Either is Right, it returns the value unchanged.
|
||||
// If the Either is Left, it applies the provided function to the error value,
|
||||
// which returns a new Either that replaces the original.
|
||||
//
|
||||
// This is useful for error recovery, fallback logic, or chaining alternative computations.
|
||||
// The error type can be widened from E1 to E2, allowing transformation of error types.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Recover from specific errors with fallback values
|
||||
// recover := either.OrElse(func(err error) either.Either[error, int] {
|
||||
// return either.Right[error](0) // default value
|
||||
// if err.Error() == "not found" {
|
||||
// return either.Right[error](0) // default value
|
||||
// }
|
||||
// return either.Left[int](err) // propagate other errors
|
||||
// })
|
||||
// result := recover(either.Left[int](errors.New("fail"))) // Right(0)
|
||||
func OrElse[E, A any](onLeft Kleisli[E, E, A]) Operator[E, A, A] {
|
||||
return Fold(onLeft, Of[E, A])
|
||||
// result := recover(either.Left[int](errors.New("not found"))) // Right(0)
|
||||
// result := recover(either.Right[error](42)) // Right(42) - unchanged
|
||||
//
|
||||
//go:inline
|
||||
func OrElse[E1, E2, A any](onLeft Kleisli[E2, E1, A]) Kleisli[E2, Either[E1, A], A] {
|
||||
return Fold(onLeft, Of[E2, A])
|
||||
}
|
||||
|
||||
// ToType attempts to convert an any value to a specific type, returning Either.
|
||||
|
||||
@@ -160,6 +160,7 @@ func TestToError(t *testing.T) {
|
||||
|
||||
// Test OrElse
|
||||
func TestOrElse(t *testing.T) {
|
||||
// Test basic recovery from Left
|
||||
recover := OrElse(func(e error) Either[error, int] {
|
||||
return Right[error](0)
|
||||
})
|
||||
@@ -167,8 +168,85 @@ func TestOrElse(t *testing.T) {
|
||||
result := recover(Left[int](errors.New("error")))
|
||||
assert.Equal(t, Right[error](0), result)
|
||||
|
||||
// Test Right value passes through unchanged
|
||||
result = recover(Right[error](42))
|
||||
assert.Equal(t, Right[error](42), result)
|
||||
|
||||
// Test selective recovery - recover some errors, propagate others
|
||||
selectiveRecover := OrElse(func(err error) Either[error, int] {
|
||||
if err.Error() == "not found" {
|
||||
return Right[error](0) // default value for "not found"
|
||||
}
|
||||
return Left[int](err) // propagate other errors
|
||||
})
|
||||
assert.Equal(t, Right[error](0), selectiveRecover(Left[int](errors.New("not found"))))
|
||||
permissionErr := errors.New("permission denied")
|
||||
assert.Equal(t, Left[int](permissionErr), selectiveRecover(Left[int](permissionErr)))
|
||||
|
||||
// Test chaining multiple OrElse operations
|
||||
firstRecover := OrElse(func(err error) Either[error, int] {
|
||||
if err.Error() == "error1" {
|
||||
return Right[error](1)
|
||||
}
|
||||
return Left[int](err)
|
||||
})
|
||||
secondRecover := OrElse(func(err error) Either[error, int] {
|
||||
if err.Error() == "error2" {
|
||||
return Right[error](2)
|
||||
}
|
||||
return Left[int](err)
|
||||
})
|
||||
assert.Equal(t, Right[error](1), F.Pipe1(Left[int](errors.New("error1")), firstRecover))
|
||||
assert.Equal(t, Right[error](2), F.Pipe1(Left[int](errors.New("error2")), F.Flow2(firstRecover, secondRecover)))
|
||||
}
|
||||
|
||||
// Test OrElseW
|
||||
func TestOrElseW(t *testing.T) {
|
||||
type ValidationError string
|
||||
type AppError int
|
||||
|
||||
// Test with Right value - should return Right with widened error type
|
||||
rightValue := Right[ValidationError]("success")
|
||||
recoverValidation := OrElse(func(ve ValidationError) Either[AppError, string] {
|
||||
return Left[string](AppError(400))
|
||||
})
|
||||
result := recoverValidation(rightValue)
|
||||
assert.True(t, IsRight(result))
|
||||
assert.Equal(t, "success", F.Pipe1(result, GetOrElse(F.Constant1[AppError](""))))
|
||||
|
||||
// Test with Left value - should apply recovery with new error type
|
||||
leftValue := Left[string](ValidationError("invalid input"))
|
||||
result = recoverValidation(leftValue)
|
||||
assert.True(t, IsLeft(result))
|
||||
_, leftVal := Unwrap(result)
|
||||
assert.Equal(t, AppError(400), leftVal)
|
||||
|
||||
// Test error type conversion - ValidationError to AppError
|
||||
convertError := OrElse(func(ve ValidationError) Either[AppError, int] {
|
||||
return Left[int](AppError(len(ve)))
|
||||
})
|
||||
converted := convertError(Left[int](ValidationError("short")))
|
||||
assert.True(t, IsLeft(converted))
|
||||
_, leftConv := Unwrap(converted)
|
||||
assert.Equal(t, AppError(5), leftConv)
|
||||
|
||||
// Test recovery to Right with widened error type
|
||||
recoverToRight := OrElse(func(ve ValidationError) Either[AppError, int] {
|
||||
if ve == "recoverable" {
|
||||
return Right[AppError](99)
|
||||
}
|
||||
return Left[int](AppError(500))
|
||||
})
|
||||
assert.Equal(t, Right[AppError](99), recoverToRight(Left[int](ValidationError("recoverable"))))
|
||||
assert.True(t, IsLeft(recoverToRight(Left[int](ValidationError("fatal")))))
|
||||
|
||||
// Test that Right values are preserved with widened error type
|
||||
preservedRight := Right[ValidationError](42)
|
||||
preserveRecover := OrElse(func(ve ValidationError) Either[AppError, int] {
|
||||
return Left[int](AppError(999))
|
||||
})
|
||||
preserved := preserveRecover(preservedRight)
|
||||
assert.Equal(t, Right[AppError](42), preserved)
|
||||
}
|
||||
|
||||
// Test ToType
|
||||
|
||||
38
v2/either/filter.go
Normal file
38
v2/either/filter.go
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package either
|
||||
|
||||
// FilterOrElse filters an Either value based on a predicate.
|
||||
// If the Either is Right and the predicate returns true, returns the original Right.
|
||||
// If the Either is Right and the predicate returns false, returns Left with the error from onFalse.
|
||||
// If the Either is Left, returns the original Left without applying the predicate.
|
||||
//
|
||||
// This is useful for adding validation to Right values, converting them to Left if they don't meet certain criteria.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// isPositive := N.MoreThan(0)
|
||||
// onNegative := func(x int) error { return fmt.Errorf("%d is not positive", x) }
|
||||
// filter := either.FilterOrElse(isPositive, onNegative)
|
||||
//
|
||||
// result1 := filter(either.Right[error](5)) // Right(5)
|
||||
// result2 := filter(either.Right[error](-3)) // Left(error: "-3 is not positive")
|
||||
// result3 := filter(either.Left[int](someError)) // Left(someError)
|
||||
//
|
||||
//go:inline
|
||||
func FilterOrElse[E, A any](pred Predicate[A], onFalse func(A) E) Operator[E, A, A] {
|
||||
return Chain(FromPredicate(pred, onFalse))
|
||||
}
|
||||
143
v2/either/filter_test.go
Normal file
143
v2/either/filter_test.go
Normal file
@@ -0,0 +1,143 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package either
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFilterOrElse(t *testing.T) {
|
||||
// Test with positive predicate
|
||||
isPositive := N.MoreThan(0)
|
||||
onNegative := func(x int) error { return fmt.Errorf("%d is not positive", x) }
|
||||
filter := FilterOrElse(isPositive, onNegative)
|
||||
|
||||
// Test Right value that passes predicate
|
||||
result := filter(Right[error](5))
|
||||
assert.Equal(t, Right[error](5), result)
|
||||
|
||||
// Test Right value that fails predicate
|
||||
result = filter(Right[error](-3))
|
||||
assert.True(t, IsLeft(result))
|
||||
left, _ := UnwrapError(result)
|
||||
assert.Equal(t, 0, left) // default value for int
|
||||
|
||||
// Test Right value at boundary (zero)
|
||||
result = filter(Right[error](0))
|
||||
assert.True(t, IsLeft(result))
|
||||
|
||||
// Test Left value (should pass through unchanged)
|
||||
originalError := errors.New("original error")
|
||||
result = filter(Left[int](originalError))
|
||||
assert.Equal(t, Left[int](originalError), result)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_StringValidation(t *testing.T) {
|
||||
// Test with string length validation
|
||||
isNotEmpty := func(s string) bool { return len(s) > 0 }
|
||||
onEmpty := func(s string) error { return errors.New("string is empty") }
|
||||
filter := FilterOrElse(isNotEmpty, onEmpty)
|
||||
|
||||
// Test non-empty string
|
||||
result := filter(Right[error]("hello"))
|
||||
assert.Equal(t, Right[error]("hello"), result)
|
||||
|
||||
// Test empty string
|
||||
result = filter(Right[error](""))
|
||||
assert.True(t, IsLeft(result))
|
||||
|
||||
// Test Left value
|
||||
originalError := errors.New("validation error")
|
||||
result = filter(Left[string](originalError))
|
||||
assert.Equal(t, Left[string](originalError), result)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_ComplexPredicate(t *testing.T) {
|
||||
// Test with range validation
|
||||
inRange := func(x int) bool { return x >= 10 && x <= 100 }
|
||||
outOfRange := func(x int) error { return fmt.Errorf("%d is out of range [10, 100]", x) }
|
||||
filter := FilterOrElse(inRange, outOfRange)
|
||||
|
||||
// Test value in range
|
||||
result := filter(Right[error](50))
|
||||
assert.Equal(t, Right[error](50), result)
|
||||
|
||||
// Test value below range
|
||||
result = filter(Right[error](5))
|
||||
assert.True(t, IsLeft(result))
|
||||
|
||||
// Test value above range
|
||||
result = filter(Right[error](150))
|
||||
assert.True(t, IsLeft(result))
|
||||
|
||||
// Test boundary values
|
||||
result = filter(Right[error](10))
|
||||
assert.Equal(t, Right[error](10), result)
|
||||
|
||||
result = filter(Right[error](100))
|
||||
assert.Equal(t, Right[error](100), result)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_ChainedFilters(t *testing.T) {
|
||||
// Test chaining multiple filters
|
||||
isPositive := N.MoreThan(0)
|
||||
onNegative := func(x int) error { return fmt.Errorf("%d is not positive", x) }
|
||||
|
||||
isEven := func(x int) bool { return x%2 == 0 }
|
||||
onOdd := func(x int) error { return fmt.Errorf("%d is not even", x) }
|
||||
|
||||
filterPositive := FilterOrElse(isPositive, onNegative)
|
||||
filterEven := FilterOrElse(isEven, onOdd)
|
||||
|
||||
// Test value that passes both filters
|
||||
result := filterEven(filterPositive(Right[error](4)))
|
||||
assert.Equal(t, Right[error](4), result)
|
||||
|
||||
// Test value that fails first filter
|
||||
result = filterEven(filterPositive(Right[error](-2)))
|
||||
assert.True(t, IsLeft(result))
|
||||
|
||||
// Test value that passes first but fails second filter
|
||||
result = filterEven(filterPositive(Right[error](3)))
|
||||
assert.True(t, IsLeft(result))
|
||||
}
|
||||
|
||||
func TestFilterOrElse_WithStructs(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
// Test with struct validation
|
||||
isAdult := func(u User) bool { return u.Age >= 18 }
|
||||
onMinor := func(u User) error { return fmt.Errorf("%s is not an adult (age: %d)", u.Name, u.Age) }
|
||||
filter := FilterOrElse(isAdult, onMinor)
|
||||
|
||||
// Test adult user
|
||||
adult := User{Name: "Alice", Age: 25}
|
||||
result := filter(Right[error](adult))
|
||||
assert.Equal(t, Right[error](adult), result)
|
||||
|
||||
// Test minor user
|
||||
minor := User{Name: "Bob", Age: 16}
|
||||
result = filter(Right[error](minor))
|
||||
assert.True(t, IsLeft(result))
|
||||
}
|
||||
@@ -48,7 +48,7 @@ import (
|
||||
// eqError := eq.FromStrictEquals[error]()
|
||||
//
|
||||
// ab := strconv.Itoa
|
||||
// bc := func(s string) bool { return len(s) > 0 }
|
||||
// bc := S.IsNonEmpty
|
||||
//
|
||||
// testing.AssertLaws(t, eqError, eqInt, eqString, eq.FromStrictEquals[bool](), ab, bc)(42)
|
||||
// }
|
||||
|
||||
@@ -21,18 +21,34 @@ 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/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
// Option is a type alias for option.Option, provided for convenience
|
||||
// when working with Either and Option together.
|
||||
type (
|
||||
Option[A any] = option.Option[A]
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
Endomorphism[T any] = endomorphism.Endomorphism[T]
|
||||
Lazy[T any] = lazy.Lazy[T]
|
||||
// Option is a type alias for option.Option, provided for convenience
|
||||
// when working with Either and Option together.
|
||||
Option[A any] = option.Option[A]
|
||||
|
||||
Kleisli[E, A, B any] = reader.Reader[A, Either[E, B]]
|
||||
// Lens is an optic that focuses on a field of type T within a structure of type S.
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
|
||||
// Endomorphism represents a function from a type to itself (T -> T).
|
||||
Endomorphism[T any] = endomorphism.Endomorphism[T]
|
||||
|
||||
// Lazy represents a deferred computation that produces a value of type T.
|
||||
Lazy[T any] = lazy.Lazy[T]
|
||||
|
||||
// Kleisli represents a Kleisli arrow for the Either monad.
|
||||
// It's a function from A to Either[E, B], used for composing operations that may fail.
|
||||
Kleisli[E, A, B any] = reader.Reader[A, Either[E, B]]
|
||||
|
||||
// Operator represents a function that transforms one Either into another.
|
||||
// It takes an Either[E, A] and produces an Either[E, B].
|
||||
Operator[E, A, B any] = Kleisli[E, Either[E, A], B]
|
||||
Monoid[E, A any] = monoid.Monoid[Either[E, A]]
|
||||
|
||||
// Monoid represents a monoid structure for Either values.
|
||||
Monoid[E, A any] = monoid.Monoid[Either[E, A]]
|
||||
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
)
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
//
|
||||
// Working with predicates:
|
||||
//
|
||||
// isPositive := func(n int) bool { return n > 0 }
|
||||
// isPositive := N.MoreThan(0)
|
||||
// isEven := func(n int) bool { return n%2 == 0 }
|
||||
//
|
||||
// classify := Ternary(
|
||||
|
||||
@@ -35,7 +35,7 @@ package function
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// isPositive := func(n int) bool { return n > 0 }
|
||||
// isPositive := N.MoreThan(0)
|
||||
// double := N.Mul(2)
|
||||
// negate := func(n int) int { return -n }
|
||||
//
|
||||
@@ -45,7 +45,7 @@ package function
|
||||
//
|
||||
// // Classify numbers
|
||||
// classify := Ternary(
|
||||
// func(n int) bool { return n > 0 },
|
||||
// N.MoreThan(0),
|
||||
// Constant1[int, string]("positive"),
|
||||
// Constant1[int, string]("non-positive"),
|
||||
// )
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
package identity
|
||||
|
||||
type (
|
||||
Kleisli[A, B any] = func(A) B
|
||||
// Kleisli represents a Kleisli arrow for the Identity monad.
|
||||
// It's simply a function from A to B, as Identity has no computational context.
|
||||
Kleisli[A, B any] = func(A) B
|
||||
|
||||
// Operator represents a function that transforms values.
|
||||
// In the Identity monad, it's equivalent to Kleisli since there's no wrapping context.
|
||||
Operator[A, B any] = Kleisli[A, B]
|
||||
)
|
||||
|
||||
47
v2/idiomatic/context/readerresult/filter.go
Normal file
47
v2/idiomatic/context/readerresult/filter.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// 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 readerresult
|
||||
|
||||
import "github.com/IBM/fp-go/v2/either"
|
||||
|
||||
// FilterOrElse filters a context-aware ReaderResult value based on a predicate in an idiomatic style.
|
||||
// If the ReaderResult computation succeeds and the predicate returns true, returns the original success value.
|
||||
// If the ReaderResult computation succeeds and the predicate returns false, returns an error with the error from onFalse.
|
||||
// If the ReaderResult computation fails, returns the original error without applying the predicate.
|
||||
//
|
||||
// This is the idiomatic version that returns an Operator for use in method chaining.
|
||||
// It's useful for adding validation to successful context-aware computations, converting them to errors if they don't meet certain criteria.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// CRR "github.com/IBM/fp-go/v2/idiomatic/context/readerresult"
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// )
|
||||
//
|
||||
// isPositive := N.MoreThan(0)
|
||||
// onNegative := func(x int) error { return fmt.Errorf("%d is not positive", x) }
|
||||
//
|
||||
// result := CRR.Of(5).
|
||||
// Pipe(CRR.FilterOrElse(isPositive, onNegative))(ctx) // Ok(5)
|
||||
//
|
||||
// result2 := CRR.Of(-3).
|
||||
// Pipe(CRR.FilterOrElse(isPositive, onNegative))(ctx) // Error(error: "-3 is not positive")
|
||||
//
|
||||
//go:inline
|
||||
func FilterOrElse[A any](pred Predicate[A], onFalse func(A) error) Operator[A, A] {
|
||||
return ChainEitherK(either.FromPredicate(pred, onFalse))
|
||||
}
|
||||
@@ -393,6 +393,26 @@ func GetOrElse[A any](onLeft reader.Kleisli[context.Context, error, A]) func(Rea
|
||||
return RR.GetOrElse(onLeft)
|
||||
}
|
||||
|
||||
// OrElse recovers from a Left (error) by providing an alternative computation.
|
||||
// If the ReaderResult is Right, it returns the value unchanged.
|
||||
// If the ReaderResult is Left, it applies the provided function to the error value,
|
||||
// which returns a new ReaderResult that replaces the original.
|
||||
//
|
||||
// This is the idiomatic version that works with context.Context-based ReaderResult.
|
||||
// This is useful for error recovery, fallback logic, or chaining alternative computations.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Recover from specific errors with fallback values
|
||||
// recover := readerresult.OrElse(func(err error) readerresult.ReaderResult[int] {
|
||||
// if err.Error() == "not found" {
|
||||
// return readerresult.Of[int](0) // default value
|
||||
// }
|
||||
// return readerresult.Left[int](err) // propagate other errors
|
||||
// })
|
||||
// result := recover(readerresult.Left[int](errors.New("not found")))(ctx) // Right(0)
|
||||
// result := recover(readerresult.Of(42))(ctx) // Right(42) - unchanged
|
||||
//
|
||||
//go:inline
|
||||
func OrElse[A any](onLeft Kleisli[error, A]) Operator[A, A] {
|
||||
return RR.OrElse(WithContextK(onLeft))
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
@@ -70,5 +71,9 @@ type (
|
||||
// Prism represents an optic that focuses on a case of type A within a sum type S.
|
||||
Prism[S, A any] = prism.Prism[S, A]
|
||||
|
||||
// Trampoline represents a tail-recursive computation that can be evaluated iteratively.
|
||||
// It's used to implement stack-safe recursion.
|
||||
Trampoline[A, B any] = tailrec.Trampoline[A, B]
|
||||
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
)
|
||||
|
||||
@@ -460,7 +460,7 @@
|
||||
// 5. Use FromPredicate for validation:
|
||||
//
|
||||
// positiveInt := result.FromPredicate(
|
||||
// func(x int) bool { return x > 0 },
|
||||
// N.MoreThan(0),
|
||||
// func(x int) error { return fmt.Errorf("%d is not positive", x) },
|
||||
// )
|
||||
//
|
||||
|
||||
47
v2/idiomatic/ioresult/filter.go
Normal file
47
v2/idiomatic/ioresult/filter.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// 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 ioresult
|
||||
|
||||
import "github.com/IBM/fp-go/v2/idiomatic/result"
|
||||
|
||||
// FilterOrElse filters an IOResult value based on a predicate in an idiomatic style.
|
||||
// If the IOResult computation succeeds and the predicate returns true, returns the original success value.
|
||||
// If the IOResult computation succeeds and the predicate returns false, returns an error with the error from onFalse.
|
||||
// If the IOResult computation fails, returns the original error without applying the predicate.
|
||||
//
|
||||
// This is the idiomatic version that returns an Operator for use in method chaining.
|
||||
// It's useful for adding validation to successful IO computations, converting them to errors if they don't meet certain criteria.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// IO "github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// )
|
||||
//
|
||||
// isPositive := N.MoreThan(0)
|
||||
// onNegative := func(x int) error { return fmt.Errorf("%d is not positive", x) }
|
||||
//
|
||||
// result := IO.Of(5).
|
||||
// Pipe(IO.FilterOrElse(isPositive, onNegative))() // Ok(5)
|
||||
//
|
||||
// result2 := IO.Of(-3).
|
||||
// Pipe(IO.FilterOrElse(isPositive, onNegative))() // Error(error: "-3 is not positive")
|
||||
//
|
||||
//go:inline
|
||||
func FilterOrElse[A any](pred Predicate[A], onFalse func(A) error) Operator[A, A] {
|
||||
return ChainResultK(result.FromPredicate(pred, onFalse))
|
||||
}
|
||||
85
v2/idiomatic/ioresult/filter_test.go
Normal file
85
v2/idiomatic/ioresult/filter_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// 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 ioresult
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFilterOrElse(t *testing.T) {
|
||||
// Test with positive predicate
|
||||
isPositive := N.MoreThan(0)
|
||||
onNegative := func(x int) error { return fmt.Errorf("%d is not positive", x) }
|
||||
|
||||
// Test value that passes predicate
|
||||
result, err := F.Pipe2(5, Of, FilterOrElse(isPositive, onNegative))()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 5, result)
|
||||
|
||||
// Test value that fails predicate
|
||||
_, err = F.Pipe2(-3, Of, FilterOrElse(isPositive, onNegative))()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "-3 is not positive", err.Error())
|
||||
|
||||
// Test error value (should pass through unchanged)
|
||||
originalError := errors.New("original error")
|
||||
_, err = F.Pipe2(originalError, Left[int], FilterOrElse(isPositive, onNegative))()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, originalError, err)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_WithChain(t *testing.T) {
|
||||
// Test FilterOrElse in a chain with other IO operations
|
||||
isPositive := N.MoreThan(0)
|
||||
onNegative := func(x int) error { return fmt.Errorf("%d is not positive", x) }
|
||||
|
||||
double := func(x int) (int, error) { return x * 2, nil }
|
||||
|
||||
// Test successful chain
|
||||
result, err := F.Pipe3(5, Of, FilterOrElse(isPositive, onNegative), ChainResultK(double))()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 10, result)
|
||||
|
||||
// Test chain with filter failure
|
||||
_, err = F.Pipe3(-5, Of, FilterOrElse(isPositive, onNegative), ChainResultK(double))()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not positive")
|
||||
}
|
||||
|
||||
func TestFilterOrElse_MultipleFilters(t *testing.T) {
|
||||
// Test chaining multiple filters
|
||||
isPositive := N.MoreThan(0)
|
||||
onNegative := func(x int) error { return fmt.Errorf("%d is not positive", x) }
|
||||
|
||||
isEven := func(x int) bool { return x%2 == 0 }
|
||||
onOdd := func(x int) error { return fmt.Errorf("%d is not even", x) }
|
||||
|
||||
// Test value that passes both filters
|
||||
result, err := F.Pipe3(4, Of, FilterOrElse(isPositive, onNegative), FilterOrElse(isEven, onOdd))()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 4, result)
|
||||
|
||||
// Test value that fails second filter
|
||||
_, err = F.Pipe3(3, Of, FilterOrElse(isPositive, onNegative), FilterOrElse(isEven, onOdd))()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not even")
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
@@ -36,4 +37,6 @@ type (
|
||||
// Operator represents a transformation from IOResult[A] to IOResult[B].
|
||||
// It is commonly used in function composition pipelines.
|
||||
Operator[A, B any] = Kleisli[IOResult[A], B]
|
||||
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
)
|
||||
|
||||
@@ -73,7 +73,7 @@ func BenchmarkChain(b *testing.B) {
|
||||
|
||||
func BenchmarkFilter(b *testing.B) {
|
||||
v, ok := Some(42)
|
||||
filter := Filter(func(x int) bool { return x > 0 })
|
||||
filter := Filter(N.MoreThan(0))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -152,7 +152,7 @@ func BenchmarkDoBind(b *testing.B) {
|
||||
|
||||
// Benchmark conversions
|
||||
func BenchmarkFromPredicate(b *testing.B) {
|
||||
pred := FromPredicate(func(x int) bool { return x > 0 })
|
||||
pred := FromPredicate(N.MoreThan(0))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
//
|
||||
// Filter keeps values that satisfy a predicate:
|
||||
//
|
||||
// isPositive := Filter(func(x int) bool { return x > 0 })
|
||||
// isPositive := Filter(N.MoreThan(0))
|
||||
// result := isPositive(Some(5)) // (5, true)
|
||||
// result := isPositive(Some(-1)) // (0, false)
|
||||
//
|
||||
@@ -127,7 +127,7 @@
|
||||
//
|
||||
// Convert predicates to Options:
|
||||
//
|
||||
// isPositive := FromPredicate(func(n int) bool { return n > 0 })
|
||||
// isPositive := FromPredicate(N.MoreThan(0))
|
||||
// result := isPositive(5) // (5, true)
|
||||
// result := isPositive(-1) // (0, false)
|
||||
//
|
||||
|
||||
@@ -47,7 +47,7 @@ import (
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// isPositive := FromPredicate(func(n int) bool { return n > 0 })
|
||||
// isPositive := FromPredicate(N.MoreThan(0))
|
||||
// result := isPositive(5) // Some(5)
|
||||
// result := isPositive(-1) // None
|
||||
func FromPredicate[A any](pred func(A) bool) Kleisli[A, A] {
|
||||
@@ -330,7 +330,7 @@ func Reduce[A, B any](f func(B, A) B, initial B) func(A, bool) B {
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// isPositive := Filter(func(x int) bool { return x > 0 })
|
||||
// isPositive := Filter(N.MoreThan(0))
|
||||
// result := isPositive(Some(5)) // Some(5)
|
||||
// result := isPositive(Some(-1)) // None
|
||||
// result := isPositive(None[int]()) // None
|
||||
|
||||
@@ -130,20 +130,20 @@ func TestChainFirst(t *testing.T) {
|
||||
// Test Filter
|
||||
func TestFilter(t *testing.T) {
|
||||
t.Run("positive case - predicate satisfied", func(t *testing.T) {
|
||||
isPositive := Filter(func(x int) bool { return x > 0 })
|
||||
isPositive := Filter(N.MoreThan(0))
|
||||
// Should keep value when predicate is satisfied
|
||||
AssertEq(Some(5))(isPositive(Some(5)))(t)
|
||||
})
|
||||
|
||||
t.Run("negative case - predicate not satisfied", func(t *testing.T) {
|
||||
isPositive := Filter(func(x int) bool { return x > 0 })
|
||||
isPositive := Filter(N.MoreThan(0))
|
||||
// Should return None when predicate fails
|
||||
AssertEq(None[int]())(isPositive(Some(-1)))(t)
|
||||
AssertEq(None[int]())(isPositive(Some(0)))(t)
|
||||
})
|
||||
|
||||
t.Run("negative case - input is None", func(t *testing.T) {
|
||||
isPositive := Filter(func(x int) bool { return x > 0 })
|
||||
isPositive := Filter(N.MoreThan(0))
|
||||
// Should return None when input is None
|
||||
AssertEq(None[int]())(isPositive(None[int]()))(t)
|
||||
})
|
||||
|
||||
@@ -47,7 +47,7 @@ import (
|
||||
// eqBool := eq.FromStrictEquals[bool]()
|
||||
//
|
||||
// ab := strconv.Itoa
|
||||
// bc := func(s string) bool { return len(s) > 0 }
|
||||
// bc := S.IsNonEmpty
|
||||
//
|
||||
// assert := AssertLaws(t, eqInt, eqString, eqBool, ab, bc)
|
||||
// assert(42) // verifies laws hold for value 42
|
||||
|
||||
51
v2/idiomatic/readerioresult/filter.go
Normal file
51
v2/idiomatic/readerioresult/filter.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// 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/either"
|
||||
|
||||
// FilterOrElse filters a ReaderIOResult value based on a predicate in an idiomatic style.
|
||||
// If the ReaderIOResult computation succeeds and the predicate returns true, returns the original success value.
|
||||
// If the ReaderIOResult computation succeeds and the predicate returns false, returns an error with the error from onFalse.
|
||||
// If the ReaderIOResult computation fails, returns the original error without applying the predicate.
|
||||
//
|
||||
// This is the idiomatic version that returns an Operator for use in method chaining.
|
||||
// It's useful for adding validation to successful IO computations with dependencies, converting them to errors if they don't meet certain criteria.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// RIO "github.com/IBM/fp-go/v2/idiomatic/readerioresult"
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// )
|
||||
//
|
||||
// type Config struct {
|
||||
// MaxValue int
|
||||
// }
|
||||
//
|
||||
// isPositive := N.MoreThan(0)
|
||||
// onNegative := func(x int) error { return fmt.Errorf("%d is not positive", x) }
|
||||
//
|
||||
// result := RIO.Of[Config](5).
|
||||
// Pipe(RIO.FilterOrElse(isPositive, onNegative))(Config{MaxValue: 10})() // Ok(5)
|
||||
//
|
||||
// result2 := RIO.Of[Config](-3).
|
||||
// Pipe(RIO.FilterOrElse(isPositive, onNegative))(Config{MaxValue: 10})() // Error(error: "-3 is not positive")
|
||||
//
|
||||
//go:inline
|
||||
func FilterOrElse[R, A any](pred Predicate[A], onFalse func(A) error) Operator[R, A, A] {
|
||||
return ChainEitherK[R](either.FromPredicate(pred, onFalse))
|
||||
}
|
||||
@@ -16,9 +16,11 @@
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
"github.com/IBM/fp-go/v2/internal/chain"
|
||||
"github.com/IBM/fp-go/v2/internal/fromeither"
|
||||
"github.com/IBM/fp-go/v2/internal/fromio"
|
||||
"github.com/IBM/fp-go/v2/internal/fromreader"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
@@ -265,67 +267,67 @@ func MonadTap[R, A, B any](fa ReaderIOResult[R, A], f Kleisli[R, A, B]) ReaderIO
|
||||
return MonadChainFirst(fa, f)
|
||||
}
|
||||
|
||||
// // MonadChainEitherK chains a computation that returns an Either into a ReaderIOResult.
|
||||
// // The Either is automatically lifted into the ReaderIOResult context.
|
||||
// //
|
||||
// //go:inline
|
||||
// func MonadChainEitherK[R, A, B any](ma ReaderIOResult[R, A], f either.Kleisli[A, B]) ReaderIOResult[R, B] {
|
||||
// return fromeither.MonadChainEitherK(
|
||||
// MonadChain[R, A, B],
|
||||
// FromEither[R, B],
|
||||
// ma,
|
||||
// f,
|
||||
// )
|
||||
// }
|
||||
// MonadChainEitherK chains a computation that returns an Either into a ReaderIOResult.
|
||||
// The Either is automatically lifted into the ReaderIOResult context.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainEitherK[R, A, B any](ma ReaderIOResult[R, A], f either.Kleisli[error, A, B]) ReaderIOResult[R, B] {
|
||||
return fromeither.MonadChainEitherK(
|
||||
MonadChain[R, A, B],
|
||||
FromEither[R, B],
|
||||
ma,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// // ChainEitherK returns a function that chains an Either-returning function into ReaderIOResult.
|
||||
// // This is the curried version of MonadChainEitherK.
|
||||
// //
|
||||
// //go:inline
|
||||
// func ChainEitherK[R, A, B any](f either.Kleisli[A, B]) Operator[R, A, B] {
|
||||
// return fromeither.ChainEitherK(
|
||||
// Chain[R, A, B],
|
||||
// FromEither[R, B],
|
||||
// f,
|
||||
// )
|
||||
// }
|
||||
// ChainEitherK returns a function that chains an Either-returning function into ReaderIOResult.
|
||||
// This is the curried version of MonadChainEitherK.
|
||||
//
|
||||
//go:inline
|
||||
func ChainEitherK[R, A, B any](f either.Kleisli[error, A, B]) Operator[R, A, B] {
|
||||
return fromeither.ChainEitherK(
|
||||
Chain[R, A, B],
|
||||
FromEither[R, B],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// // MonadChainFirstEitherK chains an Either-returning computation but keeps the original value.
|
||||
// // Useful for validation or side effects that return Either.
|
||||
// //
|
||||
// //go:inline
|
||||
// func MonadChainFirstEitherK[R, A, B any](ma ReaderIOResult[R, A], f either.Kleisli[A, B]) ReaderIOResult[R, A] {
|
||||
// return fromeither.MonadChainFirstEitherK(
|
||||
// MonadChain[R, A, A],
|
||||
// MonadMap[R, B, A],
|
||||
// FromEither[R, B],
|
||||
// ma,
|
||||
// f,
|
||||
// )
|
||||
// }
|
||||
// MonadChainFirstEitherK chains an Either-returning computation but keeps the original value.
|
||||
// Useful for validation or side effects that return Either.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstEitherK[R, A, B any](ma ReaderIOResult[R, A], f either.Kleisli[error, A, B]) ReaderIOResult[R, A] {
|
||||
return fromeither.MonadChainFirstEitherK(
|
||||
MonadChain[R, A, A],
|
||||
MonadMap[R, B, A],
|
||||
FromEither[R, B],
|
||||
ma,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// //go:inline
|
||||
// func MonadTapEitherK[R, A, B any](ma ReaderIOResult[R, A], f either.Kleisli[A, B]) ReaderIOResult[R, A] {
|
||||
// return MonadChainFirstEitherK(ma, f)
|
||||
// }
|
||||
//go:inline
|
||||
func MonadTapEitherK[R, A, B any](ma ReaderIOResult[R, A], f either.Kleisli[error, A, B]) ReaderIOResult[R, A] {
|
||||
return MonadChainFirstEitherK(ma, f)
|
||||
}
|
||||
|
||||
// // ChainFirstEitherK returns a function that chains an Either computation while preserving the original value.
|
||||
// // This is the curried version of MonadChainFirstEitherK.
|
||||
// //
|
||||
// //go:inline
|
||||
// func ChainFirstEitherK[R, A, B any](f either.Kleisli[A, B]) Operator[R, A, A] {
|
||||
// return fromeither.ChainFirstEitherK(
|
||||
// Chain[R, A, A],
|
||||
// Map[R, B, A],
|
||||
// FromEither[R, B],
|
||||
// f,
|
||||
// )
|
||||
// }
|
||||
// ChainFirstEitherK returns a function that chains an Either computation while preserving the original value.
|
||||
// This is the curried version of MonadChainFirstEitherK.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstEitherK[R, A, B any](f either.Kleisli[error, A, B]) Operator[R, A, A] {
|
||||
return fromeither.ChainFirstEitherK(
|
||||
Chain[R, A, A],
|
||||
Map[R, B, A],
|
||||
FromEither[R, B],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// //go:inline
|
||||
// func TapEitherK[R, A, B any](f either.Kleisli[A, B]) Operator[R, A, A] {
|
||||
// return ChainFirstEitherK[R](f)
|
||||
// }
|
||||
//go:inline
|
||||
func TapEitherK[R, A, B any](f either.Kleisli[error, A, B]) Operator[R, A, A] {
|
||||
return ChainFirstEitherK[R](f)
|
||||
}
|
||||
|
||||
// MonadChainReaderK chains a Reader-returning computation into a ReaderIOResult.
|
||||
// The Reader is automatically lifted into the ReaderIOResult context.
|
||||
@@ -698,13 +700,15 @@ func Flatten[R, A any](mma ReaderIOResult[R, ReaderIOResult[R, A]]) ReaderIOResu
|
||||
return MonadChain(mma, function.Identity[ReaderIOResult[R, A]])
|
||||
}
|
||||
|
||||
// // FromEither lifts an Either into a ReaderIOResult context.
|
||||
// // The Either value is independent of any context or IO effects.
|
||||
// //
|
||||
// //go:inline
|
||||
// func FromEither[R, A any](t either.Either[A]) ReaderIOResult[R, A] {
|
||||
// return readerio.Of[R](t)
|
||||
// }
|
||||
// FromEither lifts an Either into a ReaderIOResult context.
|
||||
// The Either value is independent of any context or IO effects.
|
||||
func FromEither[R, A any](t either.Either[error, A]) ReaderIOResult[R, A] {
|
||||
return func(r R) IOResult[A] {
|
||||
return func() (A, error) {
|
||||
return either.Unwrap(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RightReader lifts a Reader into a ReaderIOResult, placing the result in the Right side.
|
||||
func RightReader[R, A any](ma Reader[R, A]) ReaderIOResult[R, A] {
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
@@ -54,6 +55,7 @@ type (
|
||||
// It is equivalent to Reader[R, IOResult[A]] or func(R) func() (A, error).
|
||||
ReaderIOResult[R, A any] = Reader[R, IOResult[A]]
|
||||
|
||||
// ReaderIO represents a computation that depends on an environment R and performs side effects.
|
||||
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
|
||||
|
||||
// Monoid represents a monoid structure for ReaderIOResult values.
|
||||
@@ -66,4 +68,6 @@ type (
|
||||
// Operator represents a transformation from ReaderIOResult[R, A] to ReaderIOResult[R, B].
|
||||
// It is commonly used in function composition pipelines.
|
||||
Operator[R, A, B any] = Kleisli[R, ReaderIOResult[R, A], B]
|
||||
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
)
|
||||
|
||||
51
v2/idiomatic/readerresult/filter.go
Normal file
51
v2/idiomatic/readerresult/filter.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// 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 readerresult
|
||||
|
||||
import "github.com/IBM/fp-go/v2/either"
|
||||
|
||||
// FilterOrElse filters a ReaderResult value based on a predicate in an idiomatic style.
|
||||
// If the ReaderResult computation succeeds and the predicate returns true, returns the original success value.
|
||||
// If the ReaderResult computation succeeds and the predicate returns false, returns an error with the error from onFalse.
|
||||
// If the ReaderResult computation fails, returns the original error without applying the predicate.
|
||||
//
|
||||
// This is the idiomatic version that returns an Operator for use in method chaining.
|
||||
// It's useful for adding validation to successful computations, converting them to errors if they don't meet certain criteria.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// RR "github.com/IBM/fp-go/v2/idiomatic/readerresult"
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// )
|
||||
//
|
||||
// type Config struct {
|
||||
// MaxValue int
|
||||
// }
|
||||
//
|
||||
// isPositive := N.MoreThan(0)
|
||||
// onNegative := func(x int) error { return fmt.Errorf("%d is not positive", x) }
|
||||
//
|
||||
// result := RR.Of[Config](5).
|
||||
// Pipe(RR.FilterOrElse(isPositive, onNegative))(Config{MaxValue: 10}) // Ok(5)
|
||||
//
|
||||
// result2 := RR.Of[Config](-3).
|
||||
// Pipe(RR.FilterOrElse(isPositive, onNegative))(Config{MaxValue: 10}) // Error(error: "-3 is not positive")
|
||||
//
|
||||
//go:inline
|
||||
func FilterOrElse[R, A any](pred Predicate[A], onFalse func(A) error) Operator[R, A, A] {
|
||||
return ChainEitherK[R](either.FromPredicate(pred, onFalse))
|
||||
}
|
||||
@@ -226,7 +226,7 @@ func Ap[B, R, A any](fa ReaderResult[R, A]) Operator[R, func(A) B, B] {
|
||||
// Example:
|
||||
//
|
||||
// isPositive := readerresult.FromPredicate[Config](
|
||||
// func(x int) bool { return x > 0 },
|
||||
// N.MoreThan(0),
|
||||
// func(x int) error { return fmt.Errorf("%d is not positive", x) },
|
||||
// )
|
||||
// result := isPositive(5) // Returns ReaderResult that succeeds with 5
|
||||
|
||||
@@ -181,7 +181,7 @@ func TestMonadAp(t *testing.T) {
|
||||
|
||||
func TestFromPredicate(t *testing.T) {
|
||||
isPositive := FromPredicate[MyContext](
|
||||
func(x int) bool { return x > 0 },
|
||||
N.MoreThan(0),
|
||||
func(x int) error { return fmt.Errorf("%d is not positive", x) },
|
||||
)
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
@@ -59,4 +60,6 @@ type (
|
||||
// Operator represents a transformation from ReaderResult[R, A] to ReaderResult[R, B].
|
||||
// It is commonly used in function composition pipelines.
|
||||
Operator[R, A, B any] = Kleisli[R, ReaderResult[R, A], B]
|
||||
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
)
|
||||
|
||||
@@ -216,7 +216,7 @@ func Fold[A, B any](onLeft func(error) B, onRight func(A) B) func(A, error) B {
|
||||
// Example:
|
||||
//
|
||||
// isPositive := either.FromPredicate(
|
||||
// func(x int) bool { return x > 0 },
|
||||
// N.MoreThan(0),
|
||||
// func(x int) error { return errors.New("not positive") },
|
||||
// )
|
||||
// result := isPositive(42) // Right(42)
|
||||
|
||||
@@ -429,7 +429,7 @@ func BenchmarkReduce_Left(b *testing.B) {
|
||||
// Benchmark FromPredicate
|
||||
func BenchmarkFromPredicate_Pass(b *testing.B) {
|
||||
pred := FromPredicate(
|
||||
func(x int) bool { return x > 0 },
|
||||
N.MoreThan(0),
|
||||
func(x int) error { return errBench },
|
||||
)
|
||||
b.ResetTimer()
|
||||
@@ -441,7 +441,7 @@ func BenchmarkFromPredicate_Pass(b *testing.B) {
|
||||
|
||||
func BenchmarkFromPredicate_Fail(b *testing.B) {
|
||||
pred := FromPredicate(
|
||||
func(x int) bool { return x > 0 },
|
||||
N.MoreThan(0),
|
||||
func(x int) error { return errBench },
|
||||
)
|
||||
b.ResetTimer()
|
||||
|
||||
45
v2/idiomatic/result/filter.go
Normal file
45
v2/idiomatic/result/filter.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package result
|
||||
|
||||
// FilterOrElse filters a Result value based on a predicate in an idiomatic style.
|
||||
// If the Result is Ok and the predicate returns true, returns the original Ok.
|
||||
// If the Result is Ok and the predicate returns false, returns Error with the error from onFalse.
|
||||
// If the Result is Error, returns the original Error without applying the predicate.
|
||||
//
|
||||
// This is the idiomatic version that returns an Operator for use in method chaining.
|
||||
// It's useful for adding validation to successful results, converting them to errors if they don't meet certain criteria.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// R "github.com/IBM/fp-go/v2/idiomatic/result"
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// )
|
||||
//
|
||||
// isPositive := N.MoreThan(0)
|
||||
// onNegative := func(x int) error { return fmt.Errorf("%d is not positive", x) }
|
||||
//
|
||||
// result := R.Of(5).
|
||||
// Pipe(R.FilterOrElse(isPositive, onNegative)) // Ok(5)
|
||||
//
|
||||
// result2 := R.Of(-3).
|
||||
// Pipe(R.FilterOrElse(isPositive, onNegative)) // Error(error: "-3 is not positive")
|
||||
//
|
||||
//go:inline
|
||||
func FilterOrElse[A any](pred Predicate[A], onFalse func(A) error) Operator[A, A] {
|
||||
return Chain(FromPredicate(pred, onFalse))
|
||||
}
|
||||
160
v2/idiomatic/result/filter_test.go
Normal file
160
v2/idiomatic/result/filter_test.go
Normal file
@@ -0,0 +1,160 @@
|
||||
// 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 result
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFilterOrElse(t *testing.T) {
|
||||
// Test with positive predicate
|
||||
isPositive := N.MoreThan(0)
|
||||
onNegative := func(x int) error { return fmt.Errorf("%d is not positive", x) }
|
||||
|
||||
// Test value that passes predicate
|
||||
AssertEq(Right(5))(Pipe2(5, Of, FilterOrElse(isPositive, onNegative)))(t)
|
||||
|
||||
// Test value that fails predicate
|
||||
_, err := Pipe2(-3, Of, FilterOrElse(isPositive, onNegative))
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "-3 is not positive", err.Error())
|
||||
|
||||
// Test value at boundary (zero)
|
||||
_, err = Pipe2(0, Of, FilterOrElse(isPositive, onNegative))
|
||||
assert.Error(t, err)
|
||||
|
||||
// Test error value (should pass through unchanged)
|
||||
originalError := errors.New("original error")
|
||||
_, err = Pipe2(originalError, Left[int], FilterOrElse(isPositive, onNegative))
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, originalError, err)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_StringValidation(t *testing.T) {
|
||||
// Test with string length validation
|
||||
isNotEmpty := func(s string) bool { return len(s) > 0 }
|
||||
onEmpty := func(s string) error { return errors.New("string is empty") }
|
||||
|
||||
// Test non-empty string
|
||||
AssertEq(Right("hello"))(Pipe2("hello", Of, FilterOrElse(isNotEmpty, onEmpty)))(t)
|
||||
|
||||
// Test empty string
|
||||
_, err := Pipe2("", Of, FilterOrElse(isNotEmpty, onEmpty))
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "string is empty", err.Error())
|
||||
|
||||
// Test error value
|
||||
originalError := errors.New("validation error")
|
||||
_, err = Pipe2(originalError, Left[string], FilterOrElse(isNotEmpty, onEmpty))
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, originalError, err)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_ComplexPredicate(t *testing.T) {
|
||||
// Test with range validation
|
||||
inRange := func(x int) bool { return x >= 10 && x <= 100 }
|
||||
outOfRange := func(x int) error { return fmt.Errorf("%d is out of range [10, 100]", x) }
|
||||
|
||||
// Test value in range
|
||||
AssertEq(Right(50))(Pipe2(50, Of, FilterOrElse(inRange, outOfRange)))(t)
|
||||
|
||||
// Test value below range
|
||||
_, err := Pipe2(5, Of, FilterOrElse(inRange, outOfRange))
|
||||
assert.Error(t, err)
|
||||
|
||||
// Test value above range
|
||||
_, err = Pipe2(150, Of, FilterOrElse(inRange, outOfRange))
|
||||
assert.Error(t, err)
|
||||
|
||||
// Test boundary values
|
||||
AssertEq(Right(10))(Pipe2(10, Of, FilterOrElse(inRange, outOfRange)))(t)
|
||||
AssertEq(Right(100))(Pipe2(100, Of, FilterOrElse(inRange, outOfRange)))(t)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_ChainedFilters(t *testing.T) {
|
||||
// Test chaining multiple filters
|
||||
isPositive := N.MoreThan(0)
|
||||
onNegative := func(x int) error { return fmt.Errorf("%d is not positive", x) }
|
||||
|
||||
isEven := func(x int) bool { return x%2 == 0 }
|
||||
onOdd := func(x int) error { return fmt.Errorf("%d is not even", x) }
|
||||
|
||||
// Test value that passes both filters
|
||||
AssertEq(Right(4))(Pipe3(4, Of, FilterOrElse(isPositive, onNegative), FilterOrElse(isEven, onOdd)))(t)
|
||||
|
||||
// Test value that fails first filter
|
||||
_, err := Pipe3(-2, Of, FilterOrElse(isPositive, onNegative), FilterOrElse(isEven, onOdd))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not positive")
|
||||
|
||||
// Test value that passes first but fails second filter
|
||||
_, err = Pipe3(3, Of, FilterOrElse(isPositive, onNegative), FilterOrElse(isEven, onOdd))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not even")
|
||||
}
|
||||
|
||||
func TestFilterOrElse_WithStructs(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
// Test with struct validation
|
||||
isAdult := func(u User) bool { return u.Age >= 18 }
|
||||
onMinor := func(u User) error { return fmt.Errorf("%s is not an adult (age: %d)", u.Name, u.Age) }
|
||||
|
||||
// Test adult user
|
||||
adult := User{Name: "Alice", Age: 25}
|
||||
AssertEq(Right(adult))(Pipe2(adult, Of, FilterOrElse(isAdult, onMinor)))(t)
|
||||
|
||||
// Test minor user
|
||||
minor := User{Name: "Bob", Age: 16}
|
||||
_, err := Pipe2(minor, Of, FilterOrElse(isAdult, onMinor))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Bob is not an adult")
|
||||
}
|
||||
|
||||
func TestFilterOrElse_WithChain(t *testing.T) {
|
||||
// Test FilterOrElse in a chain with other operations
|
||||
isPositive := N.MoreThan(0)
|
||||
onNegative := func(x int) error { return fmt.Errorf("%d is not positive", x) }
|
||||
|
||||
double := func(x int) (int, error) { return x * 2, nil }
|
||||
|
||||
// Test successful chain
|
||||
AssertEq(Right(10))(Pipe3(5, Of, FilterOrElse(isPositive, onNegative), Chain(double)))(t)
|
||||
|
||||
// Test chain with filter failure
|
||||
_, err := Pipe3(-5, Of, FilterOrElse(isPositive, onNegative), Chain(double))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not positive")
|
||||
}
|
||||
|
||||
func TestFilterOrElse_ErrorMessages(t *testing.T) {
|
||||
// Test that error messages are properly propagated
|
||||
isPositive := N.MoreThan(0)
|
||||
onNegative := func(x int) error { return fmt.Errorf("value %d is not positive", x) }
|
||||
|
||||
result, err := Pipe2(-5, Of, FilterOrElse(isPositive, onNegative))
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "value -5 is not positive", err.Error())
|
||||
assert.Equal(t, 0, result) // default value for int
|
||||
}
|
||||
@@ -107,7 +107,7 @@ func TestReduce(t *testing.T) {
|
||||
// TestFromPredicate tests creating Result from a predicate
|
||||
func TestFromPredicate(t *testing.T) {
|
||||
isPositive := FromPredicate(
|
||||
func(x int) bool { return x > 0 },
|
||||
N.MoreThan(0),
|
||||
func(x int) error { return fmt.Errorf("%d is not positive", x) },
|
||||
)
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ import (
|
||||
// eqError := eq.FromStrictEquals[error]()
|
||||
//
|
||||
// ab := strconv.Itoa
|
||||
// bc := func(s string) bool { return len(s) > 0 }
|
||||
// bc := S.IsNonEmpty
|
||||
//
|
||||
// testing.AssertLaws(t, eqError, eqInt, eqString, eq.FromStrictEquals[bool](), ab, bc)(42)
|
||||
// }
|
||||
|
||||
@@ -19,15 +19,27 @@ import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
)
|
||||
|
||||
// Option is a type alias for option.Option, provided for convenience
|
||||
// when working with Either and Option together.
|
||||
type (
|
||||
Option[A any] = option.Option[A]
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
// Option is a type alias for option.Option, provided for convenience
|
||||
// when working with Result and Option together.
|
||||
Option[A any] = option.Option[A]
|
||||
|
||||
// Lens is an optic that focuses on a field of type T within a structure of type S.
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
|
||||
// Endomorphism represents a function from a type to itself (T -> T).
|
||||
Endomorphism[T any] = endomorphism.Endomorphism[T]
|
||||
|
||||
Kleisli[A, B any] = func(A) (B, error)
|
||||
// Kleisli represents a Kleisli arrow for the idiomatic Result pattern.
|
||||
// It's a function from A to (B, error), following Go's idiomatic error handling.
|
||||
Kleisli[A, B any] = func(A) (B, error)
|
||||
|
||||
// Operator represents a function that transforms one Result into another.
|
||||
// It takes (A, error) and produces (B, error), following Go's idiomatic pattern.
|
||||
Operator[A, B any] = func(A, error) (B, error)
|
||||
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
)
|
||||
|
||||
@@ -180,3 +180,28 @@ func ChainLeft[EA, A, EB, HKTFA, HKTFB any](
|
||||
f func(EA) HKTFB) func(HKTFA) HKTFB {
|
||||
return fchain(ET.Fold(f, F.Flow2(ET.Right[EB, A], fof)))
|
||||
}
|
||||
|
||||
func MonadChainFirstLeft[EA, A, EB, B, HKTFA, HKTFB any](
|
||||
fchain func(HKTFA, func(ET.Either[EA, A]) HKTFA) HKTFA,
|
||||
fmap func(HKTFB, func(ET.Either[EB, B]) ET.Either[EA, A]) HKTFA,
|
||||
fof func(ET.Either[EA, A]) HKTFA,
|
||||
fa HKTFA,
|
||||
f func(EA) HKTFB) HKTFA {
|
||||
|
||||
return fchain(fa, func(e ET.Either[EA, A]) HKTFA {
|
||||
return ET.Fold(func(ea EA) HKTFA {
|
||||
return fmap(f(ea), F.Constant1[ET.Either[EB, B]](e))
|
||||
}, F.Flow2(ET.Right[EA, A], fof))(e)
|
||||
})
|
||||
}
|
||||
|
||||
func ChainFirstLeft[EA, A, EB, B, HKTFA, HKTFB any](
|
||||
fchain func(func(ET.Either[EA, A]) HKTFA) func(HKTFA) HKTFA,
|
||||
fmap func(func(ET.Either[EB, B]) ET.Either[EA, A]) func(HKTFB) HKTFA,
|
||||
fof func(ET.Either[EA, A]) HKTFA,
|
||||
f func(EA) HKTFB) func(HKTFA) HKTFA {
|
||||
|
||||
return fchain(func(e ET.Either[EA, A]) HKTFA {
|
||||
return ET.Fold(F.Flow2(f, fmap(F.Constant1[ET.Either[EB, B]](e))), F.Flow2(ET.Right[EA, A], fof))(e)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,18 +11,31 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
// IO represents a synchronous computation that cannot fail.
|
||||
// It's a function that takes no arguments and returns a value of type A.
|
||||
// Refer to [https://andywhite.xyz/posts/2021-01-27-rte-foundations/#ioltagt] for more details.
|
||||
IO[A any] = func() A
|
||||
|
||||
// IO represents a synchronous computation that cannot fail
|
||||
// refer to [https://andywhite.xyz/posts/2021-01-27-rte-foundations/#ioltagt] for more details
|
||||
IO[A any] = func() A
|
||||
// Pair represents a tuple of two values of types L and R.
|
||||
Pair[L, R any] = pair.Pair[L, R]
|
||||
|
||||
Kleisli[A, B any] = reader.Reader[A, IO[B]]
|
||||
Operator[A, B any] = Kleisli[IO[A], B]
|
||||
Monoid[A any] = M.Monoid[IO[A]]
|
||||
Semigroup[A any] = S.Semigroup[IO[A]]
|
||||
// Kleisli represents a Kleisli arrow for the IO monad.
|
||||
// It's a function from A to IO[B], used for composing IO operations.
|
||||
Kleisli[A, B any] = reader.Reader[A, IO[B]]
|
||||
|
||||
// Operator represents a function that transforms one IO into another.
|
||||
// It takes an IO[A] and produces an IO[B].
|
||||
Operator[A, B any] = Kleisli[IO[A], B]
|
||||
|
||||
// Monoid represents a monoid structure for IO values.
|
||||
Monoid[A any] = M.Monoid[IO[A]]
|
||||
|
||||
// Semigroup represents a semigroup structure for IO values.
|
||||
Semigroup[A any] = S.Semigroup[IO[A]]
|
||||
|
||||
// Consumer represents a function that consumes a value of type A.
|
||||
Consumer[A any] = consumer.Consumer[A]
|
||||
|
||||
// Seq represents an iterator sequence over values of type T.
|
||||
Seq[T any] = iter.Seq[T]
|
||||
)
|
||||
|
||||
133
v2/ioeither/ap_test.go
Normal file
133
v2/ioeither/ap_test.go
Normal file
@@ -0,0 +1,133 @@
|
||||
// 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 ioeither
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMonadApFirstExtended(t *testing.T) {
|
||||
t.Run("both Right values - returns first", func(t *testing.T) {
|
||||
first := Of[error]("first")
|
||||
second := Of[error]("second")
|
||||
result := MonadApFirst(first, second)
|
||||
assert.Equal(t, E.Of[error]("first"), result())
|
||||
})
|
||||
|
||||
t.Run("first Left - returns Left", func(t *testing.T) {
|
||||
first := Left[string](errors.New("error1"))
|
||||
second := Of[error]("second")
|
||||
result := MonadApFirst(first, second)
|
||||
assert.True(t, E.IsLeft(result()))
|
||||
})
|
||||
|
||||
t.Run("second Left - returns Left", func(t *testing.T) {
|
||||
first := Of[error]("first")
|
||||
second := Left[string](errors.New("error2"))
|
||||
result := MonadApFirst(first, second)
|
||||
assert.True(t, E.IsLeft(result()))
|
||||
})
|
||||
|
||||
t.Run("both Left - returns first Left", func(t *testing.T) {
|
||||
first := Left[string](errors.New("error1"))
|
||||
second := Left[string](errors.New("error2"))
|
||||
result := MonadApFirst(first, second)
|
||||
assert.True(t, E.IsLeft(result()))
|
||||
})
|
||||
}
|
||||
|
||||
func TestApFirstExtended(t *testing.T) {
|
||||
t.Run("composition with Map", func(t *testing.T) {
|
||||
result := F.Pipe2(
|
||||
Of[error](10),
|
||||
ApFirst[int](Of[error](20)),
|
||||
Map[error](func(x int) int { return x * 2 }),
|
||||
)
|
||||
assert.Equal(t, E.Of[error](20), result())
|
||||
})
|
||||
|
||||
t.Run("with different types", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of[error]("text"),
|
||||
ApFirst[string](Of[error](42)),
|
||||
)
|
||||
assert.Equal(t, E.Of[error]("text"), result())
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadApSecondExtended(t *testing.T) {
|
||||
t.Run("both Right values - returns second", func(t *testing.T) {
|
||||
first := Of[error]("first")
|
||||
second := Of[error]("second")
|
||||
result := MonadApSecond(first, second)
|
||||
assert.Equal(t, E.Of[error]("second"), result())
|
||||
})
|
||||
|
||||
t.Run("first Left - returns Left", func(t *testing.T) {
|
||||
first := Left[string](errors.New("error1"))
|
||||
second := Of[error]("second")
|
||||
result := MonadApSecond(first, second)
|
||||
assert.True(t, E.IsLeft(result()))
|
||||
})
|
||||
|
||||
t.Run("second Left - returns Left", func(t *testing.T) {
|
||||
first := Of[error]("first")
|
||||
second := Left[string](errors.New("error2"))
|
||||
result := MonadApSecond(first, second)
|
||||
assert.True(t, E.IsLeft(result()))
|
||||
})
|
||||
|
||||
t.Run("both Left - returns first Left", func(t *testing.T) {
|
||||
first := Left[string](errors.New("error1"))
|
||||
second := Left[string](errors.New("error2"))
|
||||
result := MonadApSecond(first, second)
|
||||
assert.True(t, E.IsLeft(result()))
|
||||
})
|
||||
}
|
||||
|
||||
func TestApSecondExtended(t *testing.T) {
|
||||
t.Run("composition with Map", func(t *testing.T) {
|
||||
result := F.Pipe2(
|
||||
Of[error](10),
|
||||
ApSecond[int](Of[error](20)),
|
||||
Map[error](func(x int) int { return x * 2 }),
|
||||
)
|
||||
assert.Equal(t, E.Of[error](40), result())
|
||||
})
|
||||
|
||||
t.Run("sequence of operations", func(t *testing.T) {
|
||||
result := F.Pipe3(
|
||||
Of[error](1),
|
||||
ApSecond[int](Of[error](2)),
|
||||
ApSecond[int](Of[error](3)),
|
||||
ApSecond[int](Of[error](4)),
|
||||
)
|
||||
assert.Equal(t, E.Of[error](4), result())
|
||||
})
|
||||
|
||||
t.Run("with different types", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of[error]("text"),
|
||||
ApSecond[string](Of[error](42)),
|
||||
)
|
||||
assert.Equal(t, E.Of[error](42), result())
|
||||
})
|
||||
}
|
||||
158
v2/ioeither/bracket_test.go
Normal file
158
v2/ioeither/bracket_test.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 ioeither
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBracket(t *testing.T) {
|
||||
t.Run("successful acquisition, use, and release", func(t *testing.T) {
|
||||
acquired := false
|
||||
used := false
|
||||
released := false
|
||||
|
||||
acquire := func() IOEither[error, string] {
|
||||
return func() E.Either[error, string] {
|
||||
acquired = true
|
||||
return E.Right[error]("resource")
|
||||
}
|
||||
}()
|
||||
|
||||
use := func(r string) IOEither[error, int] {
|
||||
return func() E.Either[error, int] {
|
||||
used = true
|
||||
assert.Equal(t, "resource", r)
|
||||
return E.Right[error](42)
|
||||
}
|
||||
}
|
||||
|
||||
release := func(r string, result E.Either[error, int]) IOEither[error, any] {
|
||||
return func() E.Either[error, any] {
|
||||
released = true
|
||||
assert.Equal(t, "resource", r)
|
||||
assert.True(t, E.IsRight(result))
|
||||
return E.Right[error, any](nil)
|
||||
}
|
||||
}
|
||||
|
||||
result := Bracket(acquire, use, release)()
|
||||
|
||||
assert.True(t, acquired)
|
||||
assert.True(t, used)
|
||||
assert.True(t, released)
|
||||
assert.Equal(t, E.Right[error](42), result)
|
||||
})
|
||||
|
||||
t.Run("acquisition fails", func(t *testing.T) {
|
||||
used := false
|
||||
released := false
|
||||
|
||||
acquire := Left[string](errors.New("acquisition failed"))
|
||||
|
||||
use := func(r string) IOEither[error, int] {
|
||||
used = true
|
||||
return Of[error](42)
|
||||
}
|
||||
|
||||
release := func(r string, result E.Either[error, int]) IOEither[error, any] {
|
||||
released = true
|
||||
return Of[error, any](nil)
|
||||
}
|
||||
|
||||
result := Bracket(acquire, use, release)()
|
||||
|
||||
assert.False(t, used)
|
||||
assert.False(t, released)
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("use fails but release is called", func(t *testing.T) {
|
||||
acquired := false
|
||||
released := false
|
||||
var releaseResult E.Either[error, int]
|
||||
|
||||
acquire := func() IOEither[error, string] {
|
||||
return func() E.Either[error, string] {
|
||||
acquired = true
|
||||
return E.Right[error]("resource")
|
||||
}
|
||||
}()
|
||||
|
||||
use := func(r string) IOEither[error, int] {
|
||||
return Left[int](errors.New("use failed"))
|
||||
}
|
||||
|
||||
release := func(r string, result E.Either[error, int]) IOEither[error, any] {
|
||||
return func() E.Either[error, any] {
|
||||
released = true
|
||||
releaseResult = result
|
||||
assert.Equal(t, "resource", r)
|
||||
return E.Right[error, any](nil)
|
||||
}
|
||||
}
|
||||
|
||||
result := Bracket(acquire, use, release)()
|
||||
|
||||
assert.True(t, acquired)
|
||||
assert.True(t, released)
|
||||
assert.True(t, E.IsLeft(result))
|
||||
assert.True(t, E.IsLeft(releaseResult))
|
||||
})
|
||||
|
||||
t.Run("release is called even when use succeeds", func(t *testing.T) {
|
||||
releaseCallCount := 0
|
||||
|
||||
acquire := Of[error]("resource")
|
||||
|
||||
use := func(r string) IOEither[error, int] {
|
||||
return Of[error](100)
|
||||
}
|
||||
|
||||
release := func(r string, result E.Either[error, int]) IOEither[error, any] {
|
||||
return func() E.Either[error, any] {
|
||||
releaseCallCount++
|
||||
return E.Right[error, any](nil)
|
||||
}
|
||||
}
|
||||
|
||||
result := Bracket(acquire, use, release)()
|
||||
|
||||
assert.Equal(t, 1, releaseCallCount)
|
||||
assert.Equal(t, E.Right[error](100), result)
|
||||
})
|
||||
|
||||
t.Run("release error overrides successful result", func(t *testing.T) {
|
||||
acquire := Of[error]("resource")
|
||||
|
||||
use := func(r string) IOEither[error, int] {
|
||||
return Of[error](42)
|
||||
}
|
||||
|
||||
release := func(r string, result E.Either[error, int]) IOEither[error, any] {
|
||||
return Left[any](errors.New("release failed"))
|
||||
}
|
||||
|
||||
result := Bracket(acquire, use, release)()
|
||||
|
||||
// According to bracket semantics, release errors are propagated
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
}
|
||||
78
v2/ioeither/file/copy.go
Normal file
78
v2/ioeither/file/copy.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
)
|
||||
|
||||
// CopyFile copies a file from source to destination path with proper resource management.
|
||||
//
|
||||
// This is a curried function that follows the "data-last" pattern, where the source path
|
||||
// is provided first, returning a function that accepts the destination path. This design
|
||||
// enables partial application and better composition with other functional operations.
|
||||
//
|
||||
// The function uses [ioeither.WithResource] to ensure both source and destination files
|
||||
// are properly closed, even if an error occurs during the copy operation. The copy is
|
||||
// performed using [io.Copy] which efficiently transfers data between the files.
|
||||
//
|
||||
// Parameters:
|
||||
// - src: The path to the source file to copy from
|
||||
//
|
||||
// Returns:
|
||||
// - A function that accepts the destination path and returns an [IOEither] that:
|
||||
// - On success: Contains the destination path (Right)
|
||||
// - On failure: Contains the error (Left) from opening, copying, or closing files
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a copy operation for a specific source file
|
||||
// copyFromSource := CopyFile("/path/to/source.txt")
|
||||
//
|
||||
// // Execute the copy to a destination
|
||||
// result := copyFromSource("/path/to/destination.txt")()
|
||||
//
|
||||
// // Or use it in a pipeline
|
||||
// result := F.Pipe1(
|
||||
// CopyFile("/path/to/source.txt"),
|
||||
// ioeither.Map(func(dst string) string {
|
||||
// return "Copied to: " + dst
|
||||
// }),
|
||||
// )("/path/to/destination.txt")()
|
||||
//
|
||||
//go:inline
|
||||
func CopyFile(src string) func(dst string) IOEither[error, string] {
|
||||
withSrc := ioeither.WithResource[int64](Open(src), Close)
|
||||
return func(dst string) IOEither[error, string] {
|
||||
withDst := ioeither.WithResource[int64](Create(dst), Close)
|
||||
|
||||
return F.Pipe1(
|
||||
withSrc(func(srcFile *os.File) IOEither[error, int64] {
|
||||
return withDst(func(dstFile *os.File) IOEither[error, int64] {
|
||||
return func() Either[error, int64] {
|
||||
return either.TryCatchError(io.Copy(dstFile, srcFile))
|
||||
}
|
||||
})
|
||||
}),
|
||||
ioeither.MapTo[error, int64](dst),
|
||||
)
|
||||
}
|
||||
}
|
||||
360
v2/ioeither/file/copy_test.go
Normal file
360
v2/ioeither/file/copy_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 file
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
IOE "github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestCopyFileSuccess tests successful file copying
|
||||
func TestCopyFileSuccess(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create source file with test content
|
||||
srcPath := filepath.Join(tempDir, "source.txt")
|
||||
testContent := []byte("Hello, CopyFile! This is test content.")
|
||||
err := os.WriteFile(srcPath, testContent, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Copy to destination
|
||||
dstPath := filepath.Join(tempDir, "destination.txt")
|
||||
result := CopyFile(srcPath)(dstPath)()
|
||||
|
||||
// Verify success
|
||||
assert.True(t, E.IsRight(result))
|
||||
returnedPath := E.GetOrElse(func(error) string { return "" })(result)
|
||||
assert.Equal(t, dstPath, returnedPath)
|
||||
|
||||
// Verify destination file content matches source
|
||||
dstContent, err := os.ReadFile(dstPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testContent, dstContent)
|
||||
}
|
||||
|
||||
// TestCopyFileEmptyFile tests copying an empty file
|
||||
func TestCopyFileEmptyFile(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create empty source file
|
||||
srcPath := filepath.Join(tempDir, "empty_source.txt")
|
||||
err := os.WriteFile(srcPath, []byte{}, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Copy to destination
|
||||
dstPath := filepath.Join(tempDir, "empty_destination.txt")
|
||||
result := CopyFile(srcPath)(dstPath)()
|
||||
|
||||
// Verify success
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify destination file is also empty
|
||||
dstContent, err := os.ReadFile(dstPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, len(dstContent))
|
||||
}
|
||||
|
||||
// TestCopyFileLargeFile tests copying a larger file
|
||||
func TestCopyFileLargeFile(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create source file with larger content (1MB)
|
||||
srcPath := filepath.Join(tempDir, "large_source.txt")
|
||||
largeContent := make([]byte, 1024*1024) // 1MB
|
||||
for i := range largeContent {
|
||||
largeContent[i] = byte(i % 256)
|
||||
}
|
||||
err := os.WriteFile(srcPath, largeContent, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Copy to destination
|
||||
dstPath := filepath.Join(tempDir, "large_destination.txt")
|
||||
result := CopyFile(srcPath)(dstPath)()
|
||||
|
||||
// Verify success
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify destination file content matches source
|
||||
dstContent, err := os.ReadFile(dstPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, largeContent, dstContent)
|
||||
}
|
||||
|
||||
// TestCopyFileSourceNotFound tests error when source file doesn't exist
|
||||
func TestCopyFileSourceNotFound(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
srcPath := filepath.Join(tempDir, "nonexistent_source.txt")
|
||||
dstPath := filepath.Join(tempDir, "destination.txt")
|
||||
|
||||
result := CopyFile(srcPath)(dstPath)()
|
||||
|
||||
// Verify failure
|
||||
assert.True(t, E.IsLeft(result))
|
||||
err := E.Fold(func(e error) error { return e }, func(string) error { return nil })(result)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// TestCopyFileDestinationDirectoryNotFound tests error when destination directory doesn't exist
|
||||
func TestCopyFileDestinationDirectoryNotFound(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create source file
|
||||
srcPath := filepath.Join(tempDir, "source.txt")
|
||||
err := os.WriteFile(srcPath, []byte("test"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try to copy to non-existent directory
|
||||
dstPath := filepath.Join(tempDir, "nonexistent_dir", "destination.txt")
|
||||
result := CopyFile(srcPath)(dstPath)()
|
||||
|
||||
// Verify failure
|
||||
assert.True(t, E.IsLeft(result))
|
||||
}
|
||||
|
||||
// TestCopyFileOverwriteExisting tests overwriting an existing destination file
|
||||
func TestCopyFileOverwriteExisting(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create source file
|
||||
srcPath := filepath.Join(tempDir, "source.txt")
|
||||
newContent := []byte("New content")
|
||||
err := os.WriteFile(srcPath, newContent, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create existing destination file with different content
|
||||
dstPath := filepath.Join(tempDir, "destination.txt")
|
||||
oldContent := []byte("Old content that should be replaced")
|
||||
err = os.WriteFile(dstPath, oldContent, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Copy and overwrite
|
||||
result := CopyFile(srcPath)(dstPath)()
|
||||
|
||||
// Verify success
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify destination has new content
|
||||
dstContent, err := os.ReadFile(dstPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, newContent, dstContent)
|
||||
}
|
||||
|
||||
// TestCopyFileCurrying tests the curried nature of CopyFile (data-last pattern)
|
||||
func TestCopyFileCurrying(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create source file
|
||||
srcPath := filepath.Join(tempDir, "source.txt")
|
||||
testContent := []byte("Currying test content")
|
||||
err := os.WriteFile(srcPath, testContent, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a partially applied function
|
||||
copyFromSource := CopyFile(srcPath)
|
||||
|
||||
// Use the partially applied function multiple times
|
||||
dst1 := filepath.Join(tempDir, "dest1.txt")
|
||||
dst2 := filepath.Join(tempDir, "dest2.txt")
|
||||
|
||||
result1 := copyFromSource(dst1)()
|
||||
result2 := copyFromSource(dst2)()
|
||||
|
||||
// Verify both copies succeeded
|
||||
assert.True(t, E.IsRight(result1))
|
||||
assert.True(t, E.IsRight(result2))
|
||||
|
||||
// Verify both destinations have the same content
|
||||
content1, err := os.ReadFile(dst1)
|
||||
require.NoError(t, err)
|
||||
content2, err := os.ReadFile(dst2)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testContent, content1)
|
||||
assert.Equal(t, testContent, content2)
|
||||
}
|
||||
|
||||
// TestCopyFileComposition tests composing CopyFile with other operations
|
||||
func TestCopyFileComposition(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create source file
|
||||
srcPath := filepath.Join(tempDir, "source.txt")
|
||||
testContent := []byte("Composition test")
|
||||
err := os.WriteFile(srcPath, testContent, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
dstPath := filepath.Join(tempDir, "destination.txt")
|
||||
|
||||
// Compose CopyFile with Map to transform the result
|
||||
result := F.Pipe1(
|
||||
CopyFile(srcPath)(dstPath),
|
||||
IOE.Map[error](func(dst string) string {
|
||||
return "Successfully copied to: " + dst
|
||||
}),
|
||||
)()
|
||||
|
||||
// Verify success and transformation
|
||||
assert.True(t, E.IsRight(result))
|
||||
message := E.GetOrElse(func(error) string { return "" })(result)
|
||||
assert.Equal(t, "Successfully copied to: "+dstPath, message)
|
||||
|
||||
// Verify file was actually copied
|
||||
dstContent, err := os.ReadFile(dstPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testContent, dstContent)
|
||||
}
|
||||
|
||||
// TestCopyFileChaining tests chaining multiple copy operations
|
||||
func TestCopyFileChaining(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create source file
|
||||
srcPath := filepath.Join(tempDir, "source.txt")
|
||||
testContent := []byte("Chaining test")
|
||||
err := os.WriteFile(srcPath, testContent, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
dst1Path := filepath.Join(tempDir, "dest1.txt")
|
||||
dst2Path := filepath.Join(tempDir, "dest2.txt")
|
||||
|
||||
// Chain two copy operations
|
||||
result := F.Pipe1(
|
||||
CopyFile(srcPath)(dst1Path),
|
||||
IOE.Chain[error](func(string) IOEither[error, string] {
|
||||
return CopyFile(dst1Path)(dst2Path)
|
||||
}),
|
||||
)()
|
||||
|
||||
// Verify success
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify both files exist with correct content
|
||||
content1, err := os.ReadFile(dst1Path)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testContent, content1)
|
||||
|
||||
content2, err := os.ReadFile(dst2Path)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testContent, content2)
|
||||
}
|
||||
|
||||
// TestCopyFileWithBinaryContent tests copying binary files
|
||||
func TestCopyFileWithBinaryContent(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create source file with binary content
|
||||
srcPath := filepath.Join(tempDir, "binary_source.bin")
|
||||
binaryContent := []byte{0x00, 0x01, 0x02, 0xFF, 0xFE, 0xFD, 0x7F, 0x80}
|
||||
err := os.WriteFile(srcPath, binaryContent, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Copy to destination
|
||||
dstPath := filepath.Join(tempDir, "binary_destination.bin")
|
||||
result := CopyFile(srcPath)(dstPath)()
|
||||
|
||||
// Verify success
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify binary content is preserved
|
||||
dstContent, err := os.ReadFile(dstPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, binaryContent, dstContent)
|
||||
}
|
||||
|
||||
// TestCopyFileErrorHandling tests error handling with Either operations
|
||||
func TestCopyFileErrorHandling(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
srcPath := filepath.Join(tempDir, "nonexistent.txt")
|
||||
dstPath := filepath.Join(tempDir, "destination.txt")
|
||||
|
||||
result := CopyFile(srcPath)(dstPath)()
|
||||
|
||||
// Test error handling with Fold
|
||||
message := E.Fold(
|
||||
func(err error) string { return "Error: " + err.Error() },
|
||||
func(dst string) string { return "Success: " + dst },
|
||||
)(result)
|
||||
|
||||
assert.Contains(t, message, "Error:")
|
||||
}
|
||||
|
||||
// TestCopyFileResourceCleanup tests that resources are properly cleaned up
|
||||
func TestCopyFileResourceCleanup(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create source file
|
||||
srcPath := filepath.Join(tempDir, "source.txt")
|
||||
testContent := []byte("Resource cleanup test")
|
||||
err := os.WriteFile(srcPath, testContent, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
dstPath := filepath.Join(tempDir, "destination.txt")
|
||||
|
||||
// Perform copy
|
||||
result := CopyFile(srcPath)(dstPath)()
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify we can immediately delete both files (no file handles left open)
|
||||
err = os.Remove(srcPath)
|
||||
assert.NoError(t, err, "Source file should be closed and deletable")
|
||||
|
||||
err = os.Remove(dstPath)
|
||||
assert.NoError(t, err, "Destination file should be closed and deletable")
|
||||
}
|
||||
|
||||
// TestCopyFileMultipleOperations tests using CopyFile multiple times independently
|
||||
func TestCopyFileMultipleOperations(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create multiple source files
|
||||
src1 := filepath.Join(tempDir, "source1.txt")
|
||||
src2 := filepath.Join(tempDir, "source2.txt")
|
||||
content1 := []byte("Content 1")
|
||||
content2 := []byte("Content 2")
|
||||
|
||||
err := os.WriteFile(src1, content1, 0644)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(src2, content2, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Perform multiple independent copies
|
||||
dst1 := filepath.Join(tempDir, "dest1.txt")
|
||||
dst2 := filepath.Join(tempDir, "dest2.txt")
|
||||
|
||||
result1 := CopyFile(src1)(dst1)()
|
||||
result2 := CopyFile(src2)(dst2)()
|
||||
|
||||
// Verify both succeeded
|
||||
assert.True(t, E.IsRight(result1))
|
||||
assert.True(t, E.IsRight(result2))
|
||||
|
||||
// Verify correct content in each destination
|
||||
dstContent1, err := os.ReadFile(dst1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, content1, dstContent1)
|
||||
|
||||
dstContent2, err := os.ReadFile(dst2)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, content2, dstContent2)
|
||||
}
|
||||
21
v2/ioeither/file/coverage.out
Normal file
21
v2/ioeither/file/coverage.out
Normal file
@@ -0,0 +1,21 @@
|
||||
mode: set
|
||||
github.com/IBM/fp-go/v2/ioeither/file/dir.go:40.70,41.55 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/dir.go:41.55,43.3 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/dir.go:62.67,63.55 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/dir.go:63.55,65.3 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/file.go:77.81,78.51 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/file.go:78.51,79.56 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/file.go:79.56,81.4 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/file.go:86.50,87.55 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/file.go:87.55,89.3 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/file.go:93.51,94.52 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/file.go:94.52,96.3 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/read.go:76.106,80.2 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/readall.go:41.83,49.2 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/tempfile.go:42.76,44.2 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/write.go:24.69,25.43 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/write.go:25.43,26.56 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/write.go:26.56,29.4 2 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/write.go:45.73,47.67 2 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/write.go:47.67,53.3 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/write.go:71.105,75.2 1 1
|
||||
@@ -21,14 +21,44 @@ import (
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
)
|
||||
|
||||
// MkdirAll create a sequence of directories, see [os.MkdirAll]
|
||||
// MkdirAll creates a directory and all necessary parent directories with the specified permissions.
|
||||
// If the directory already exists, MkdirAll does nothing and returns success.
|
||||
// This is equivalent to the Unix command `mkdir -p`.
|
||||
//
|
||||
// The perm parameter specifies the Unix permission bits for the created directories.
|
||||
// Common values include 0755 (rwxr-xr-x) for directories.
|
||||
//
|
||||
// Returns an IOEither that, when executed, creates the directory structure and returns
|
||||
// the path on success or an error on failure.
|
||||
//
|
||||
// See [os.MkdirAll] for more details.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// mkdirOp := MkdirAll("/tmp/my/nested/dir", 0755)
|
||||
// result := mkdirOp() // Either[error, string]
|
||||
func MkdirAll(path string, perm os.FileMode) IOEither[error, string] {
|
||||
return ioeither.TryCatchError(func() (string, error) {
|
||||
return path, os.MkdirAll(path, perm)
|
||||
})
|
||||
}
|
||||
|
||||
// Mkdir create a directory, see [os.Mkdir]
|
||||
// Mkdir creates a single directory with the specified permissions.
|
||||
// Unlike MkdirAll, it returns an error if the parent directory does not exist
|
||||
// or if the directory already exists.
|
||||
//
|
||||
// The perm parameter specifies the Unix permission bits for the created directory.
|
||||
// Common values include 0755 (rwxr-xr-x) for directories.
|
||||
//
|
||||
// Returns an IOEither that, when executed, creates the directory and returns
|
||||
// the path on success or an error on failure.
|
||||
//
|
||||
// See [os.Mkdir] for more details.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// mkdirOp := Mkdir("/tmp/mydir", 0755)
|
||||
// result := mkdirOp() // Either[error, string]
|
||||
func Mkdir(path string, perm os.FileMode) IOEither[error, string] {
|
||||
return ioeither.TryCatchError(func() (string, error) {
|
||||
return path, os.Mkdir(path, perm)
|
||||
|
||||
153
v2/ioeither/file/dir_test.go
Normal file
153
v2/ioeither/file/dir_test.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 file
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMkdirAll(t *testing.T) {
|
||||
// Create a temporary directory for testing
|
||||
tempDir := t.TempDir()
|
||||
|
||||
t.Run("creates nested directories", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "nested", "dir", "structure")
|
||||
|
||||
result := MkdirAll(testPath, 0755)()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
path := E.GetOrElse(func(error) string { return "" })(result)
|
||||
assert.Equal(t, testPath, path)
|
||||
|
||||
// Verify directory was created
|
||||
info, err := os.Stat(testPath)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, info.IsDir())
|
||||
})
|
||||
|
||||
t.Run("succeeds when directory already exists", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "existing")
|
||||
|
||||
// Create directory first
|
||||
err := os.Mkdir(testPath, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try to create again
|
||||
result := MkdirAll(testPath, 0755)()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
path := E.GetOrElse(func(error) string { return "" })(result)
|
||||
assert.Equal(t, testPath, path)
|
||||
})
|
||||
|
||||
t.Run("fails with invalid path", func(t *testing.T) {
|
||||
// Use a path that contains a file as a parent
|
||||
filePath := filepath.Join(tempDir, "file.txt")
|
||||
err := os.WriteFile(filePath, []byte("test"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
invalidPath := filepath.Join(filePath, "subdir")
|
||||
result := MkdirAll(invalidPath, 0755)()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("creates directory with correct permissions", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "perms")
|
||||
|
||||
result := MkdirAll(testPath, 0700)()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify permissions (on Unix-like systems)
|
||||
info, err := os.Stat(testPath)
|
||||
require.NoError(t, err)
|
||||
// Note: actual permissions may differ due to umask
|
||||
assert.True(t, info.IsDir())
|
||||
})
|
||||
}
|
||||
|
||||
func TestMkdir(t *testing.T) {
|
||||
// Create a temporary directory for testing
|
||||
tempDir := t.TempDir()
|
||||
|
||||
t.Run("creates single directory", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "single")
|
||||
|
||||
result := Mkdir(testPath, 0755)()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
path := E.GetOrElse(func(error) string { return "" })(result)
|
||||
assert.Equal(t, testPath, path)
|
||||
|
||||
// Verify directory was created
|
||||
info, err := os.Stat(testPath)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, info.IsDir())
|
||||
})
|
||||
|
||||
t.Run("fails when parent does not exist", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "nonexistent", "child")
|
||||
|
||||
result := Mkdir(testPath, 0755)()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails when directory already exists", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "existing2")
|
||||
|
||||
// Create directory first
|
||||
err := os.Mkdir(testPath, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try to create again
|
||||
result := Mkdir(testPath, 0755)()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails with invalid path", func(t *testing.T) {
|
||||
// Use a path that contains a file as a parent
|
||||
filePath := filepath.Join(tempDir, "file2.txt")
|
||||
err := os.WriteFile(filePath, []byte("test"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
invalidPath := filepath.Join(filePath, "subdir")
|
||||
result := Mkdir(invalidPath, 0755)()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("creates directory with correct permissions", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "perms2")
|
||||
|
||||
result := Mkdir(testPath, 0700)()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify permissions (on Unix-like systems)
|
||||
info, err := os.Stat(testPath)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, info.IsDir())
|
||||
})
|
||||
}
|
||||
@@ -90,8 +90,8 @@ func Remove(name string) IOEither[error, string] {
|
||||
}
|
||||
|
||||
// Close closes an object
|
||||
func Close[C io.Closer](c C) IOEither[error, any] {
|
||||
return ioeither.TryCatchError(func() (any, error) {
|
||||
return c, c.Close()
|
||||
func Close[C io.Closer](c C) IOEither[error, struct{}] {
|
||||
return ioeither.TryCatchError(func() (struct{}, error) {
|
||||
return struct{}{}, c.Close()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -248,9 +248,8 @@ func TestReadComposition(t *testing.T) {
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var num int
|
||||
// Simple parsing
|
||||
num = int(data[0]-'0')*10 + int(data[1]-'0')
|
||||
num := int(data[0]-'0')*10 + int(data[1]-'0')
|
||||
return num, nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -28,7 +28,16 @@ var (
|
||||
readAll = ioeither.Eitherize1(io.ReadAll)
|
||||
)
|
||||
|
||||
// ReadAll uses a generator function to create a stream, reads it and closes it
|
||||
// ReadAll reads all data from a ReadCloser and ensures it is properly closed.
|
||||
// It takes an IOEither that acquires the ReadCloser, reads all its content until EOF,
|
||||
// and automatically closes the reader, even if an error occurs during reading.
|
||||
//
|
||||
// This is the recommended way to read entire files with proper resource management.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// readOp := ReadAll(Open("input.txt"))
|
||||
// result := readOp() // Either[error, []byte]
|
||||
func ReadAll[R io.ReadCloser](acquire IOEither[error, R]) IOEither[error, []byte] {
|
||||
return F.Pipe1(
|
||||
F.Flow2(
|
||||
|
||||
150
v2/ioeither/file/readall_test.go
Normal file
150
v2/ioeither/file/readall_test.go
Normal file
@@ -0,0 +1,150 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestReadAll(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
t.Run("reads entire file", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "readall.txt")
|
||||
testData := []byte("Hello, ReadAll!")
|
||||
|
||||
// Create test file
|
||||
err := os.WriteFile(testPath, testData, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Read file
|
||||
result := ReadAll(Open(testPath))()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
data := E.GetOrElse(func(error) []byte { return nil })(result)
|
||||
assert.Equal(t, testData, data)
|
||||
})
|
||||
|
||||
t.Run("reads empty file", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "empty.txt")
|
||||
|
||||
// Create empty file
|
||||
err := os.WriteFile(testPath, []byte{}, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Read file
|
||||
result := ReadAll(Open(testPath))()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
data := E.GetOrElse(func(error) []byte { return nil })(result)
|
||||
assert.Equal(t, 0, len(data))
|
||||
})
|
||||
|
||||
t.Run("reads large file", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "large.txt")
|
||||
|
||||
// Create large file (1MB)
|
||||
largeData := make([]byte, 1024*1024)
|
||||
for i := range largeData {
|
||||
largeData[i] = byte(i % 256)
|
||||
}
|
||||
err := os.WriteFile(testPath, largeData, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Read file
|
||||
result := ReadAll(Open(testPath))()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
data := E.GetOrElse(func(error) []byte { return nil })(result)
|
||||
assert.Equal(t, len(largeData), len(data))
|
||||
assert.Equal(t, largeData, data)
|
||||
})
|
||||
|
||||
t.Run("fails when file does not exist", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "nonexistent.txt")
|
||||
|
||||
// Try to read non-existent file
|
||||
result := ReadAll(Open(testPath))()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails when trying to read directory", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "dir")
|
||||
err := os.Mkdir(testPath, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try to read directory
|
||||
result := ReadAll(Open(testPath))()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("reads file with special characters", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "special.txt")
|
||||
testData := []byte("Hello\nWorld\t!\r\n")
|
||||
|
||||
// Create test file
|
||||
err := os.WriteFile(testPath, testData, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Read file
|
||||
result := ReadAll(Open(testPath))()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
data := E.GetOrElse(func(error) []byte { return nil })(result)
|
||||
assert.Equal(t, testData, data)
|
||||
})
|
||||
|
||||
t.Run("reads binary file", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "binary.bin")
|
||||
testData := []byte{0x00, 0x01, 0x02, 0xFF, 0xFE, 0xFD}
|
||||
|
||||
// Create binary file
|
||||
err := os.WriteFile(testPath, testData, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Read file
|
||||
result := ReadAll(Open(testPath))()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
data := E.GetOrElse(func(error) []byte { return nil })(result)
|
||||
assert.Equal(t, testData, data)
|
||||
})
|
||||
|
||||
t.Run("closes file after reading", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "close_test.txt")
|
||||
testData := []byte("test")
|
||||
|
||||
// Create test file
|
||||
err := os.WriteFile(testPath, testData, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Read file
|
||||
result := ReadAll(Open(testPath))()
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify we can delete the file (it's closed)
|
||||
err = os.Remove(testPath)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
)
|
||||
|
||||
type (
|
||||
Either[E, T any] = either.Either[E, T]
|
||||
IOEither[E, T any] = ioeither.IOEither[E, T]
|
||||
Kleisli[E, A, B any] = ioeither.Kleisli[E, A, B]
|
||||
Operator[E, A, B any] = ioeither.Operator[E, A, B]
|
||||
|
||||
@@ -30,7 +30,18 @@ func onWriteAll[W io.Writer](data []byte) Kleisli[error, W, []byte] {
|
||||
}
|
||||
}
|
||||
|
||||
// WriteAll uses a generator function to create a stream, writes data to it and closes it
|
||||
// WriteAll writes data to a WriteCloser and ensures it is properly closed.
|
||||
// It takes the data to write and returns an Operator that accepts an IOEither
|
||||
// that creates the WriteCloser. The WriteCloser is automatically closed after
|
||||
// the write operation, even if an error occurs.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// writeOp := F.Pipe2(
|
||||
// Open("output.txt"),
|
||||
// WriteAll([]byte("Hello, World!")),
|
||||
// )
|
||||
// result := writeOp() // Either[error, []byte]
|
||||
func WriteAll[W io.WriteCloser](data []byte) Operator[error, W, []byte] {
|
||||
onWrite := onWriteAll[W](data)
|
||||
return func(onCreate IOEither[error, W]) IOEither[error, []byte] {
|
||||
@@ -42,7 +53,21 @@ func WriteAll[W io.WriteCloser](data []byte) Operator[error, W, []byte] {
|
||||
}
|
||||
}
|
||||
|
||||
// Write uses a generator function to create a stream, writes data to it and closes it
|
||||
// Write creates a resource-safe writer that automatically manages the lifecycle of a WriteCloser.
|
||||
// It takes an IOEither that acquires the WriteCloser and returns a Kleisli arrow that accepts
|
||||
// a write operation. The WriteCloser is automatically closed after the operation completes,
|
||||
// even if an error occurs.
|
||||
//
|
||||
// This is useful for composing multiple write operations with proper resource management.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// writeOp := Write[int](Open("output.txt"))
|
||||
// result := writeOp(func(f *os.File) IOEither[error, int] {
|
||||
// return ioeither.TryCatchError(func() (int, error) {
|
||||
// return f.Write([]byte("Hello"))
|
||||
// })
|
||||
// })
|
||||
func Write[R any, W io.WriteCloser](acquire IOEither[error, W]) Kleisli[error, Kleisli[error, W, R], R] {
|
||||
return ioeither.WithResource[R](
|
||||
acquire,
|
||||
|
||||
234
v2/ioeither/file/write_test.go
Normal file
234
v2/ioeither/file/write_test.go
Normal file
@@ -0,0 +1,234 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
IOE "github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWriteAll(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
t.Run("writes data and closes file", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "writeall.txt")
|
||||
testData := []byte("Hello, WriteAll!")
|
||||
|
||||
// Create and write to file
|
||||
result := WriteAll[*os.File](testData)(Create(testPath))()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
data := E.GetOrElse(func(error) []byte { return nil })(result)
|
||||
assert.Equal(t, testData, data)
|
||||
|
||||
// Verify file contents
|
||||
content, err := os.ReadFile(testPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testData, content)
|
||||
})
|
||||
|
||||
t.Run("handles write errors", func(t *testing.T) {
|
||||
// Try to write to a directory (should fail)
|
||||
testPath := filepath.Join(tempDir, "dir_not_file")
|
||||
err := os.Mkdir(testPath, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
testData := []byte("This should fail")
|
||||
|
||||
result := WriteAll[*os.File](testData)(Create(testPath))()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("writes empty data", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "empty.txt")
|
||||
testData := []byte{}
|
||||
|
||||
result := WriteAll[*os.File](testData)(Create(testPath))()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify file is empty
|
||||
content, err := os.ReadFile(testPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, len(content))
|
||||
})
|
||||
|
||||
t.Run("overwrites existing file", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "overwrite.txt")
|
||||
|
||||
// Write initial content
|
||||
err := os.WriteFile(testPath, []byte("old content"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Overwrite with new content
|
||||
newData := []byte("new content")
|
||||
result := WriteAll[*os.File](newData)(Create(testPath))()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify new content
|
||||
content, err := os.ReadFile(testPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, newData, content)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWrite(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
t.Run("writes data with resource management", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "write.txt")
|
||||
testData := []byte("Hello, Write!")
|
||||
|
||||
writeOp := Write[int](Create(testPath))
|
||||
result := writeOp(func(f *os.File) IOEither[error, int] {
|
||||
return IOE.TryCatchError(func() (int, error) {
|
||||
return f.Write(testData)
|
||||
})
|
||||
})()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
n := E.GetOrElse(func(error) int { return 0 })(result)
|
||||
assert.Equal(t, len(testData), n)
|
||||
|
||||
// Verify file contents
|
||||
content, err := os.ReadFile(testPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testData, content)
|
||||
})
|
||||
|
||||
t.Run("handles multiple writes", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "multiwrite.txt")
|
||||
|
||||
writeOp := Write[int](Create(testPath))
|
||||
result := writeOp(func(f *os.File) IOEither[error, int] {
|
||||
return IOE.TryCatchError(func() (int, error) {
|
||||
n1, err := f.Write([]byte("First "))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n2, err := f.Write([]byte("Second"))
|
||||
return n1 + n2, err
|
||||
})
|
||||
})()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify file contents
|
||||
content, err := os.ReadFile(testPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "First Second", string(content))
|
||||
})
|
||||
|
||||
t.Run("closes file even on error", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "error.txt")
|
||||
|
||||
writeOp := Write[int](Create(testPath))
|
||||
result := writeOp(func(f *os.File) IOEither[error, int] {
|
||||
// Close the file prematurely to cause an error
|
||||
f.Close()
|
||||
return IOE.TryCatchError(func() (int, error) {
|
||||
return f.Write([]byte("This should fail"))
|
||||
})
|
||||
})()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
func TestWriteFile(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
t.Run("writes file successfully", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "writefile.txt")
|
||||
testData := []byte("Hello, WriteFile!")
|
||||
|
||||
result := WriteFile(testPath, 0644)(testData)()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
data := E.GetOrElse(func(error) []byte { return nil })(result)
|
||||
assert.Equal(t, testData, data)
|
||||
|
||||
// Verify file contents
|
||||
content, err := os.ReadFile(testPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testData, content)
|
||||
})
|
||||
|
||||
t.Run("creates file with correct permissions", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "perms.txt")
|
||||
testData := []byte("test")
|
||||
|
||||
result := WriteFile(testPath, 0600)(testData)()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify file exists
|
||||
info, err := os.Stat(testPath)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, info.IsDir())
|
||||
})
|
||||
|
||||
t.Run("overwrites existing file", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "overwrite2.txt")
|
||||
|
||||
// Write initial content
|
||||
err := os.WriteFile(testPath, []byte("old"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Overwrite
|
||||
newData := []byte("new")
|
||||
result := WriteFile(testPath, 0644)(newData)()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify new content
|
||||
content, err := os.ReadFile(testPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, newData, content)
|
||||
})
|
||||
|
||||
t.Run("fails with invalid path", func(t *testing.T) {
|
||||
// Try to write to a directory that doesn't exist
|
||||
testPath := filepath.Join(tempDir, "nonexistent", "file.txt")
|
||||
testData := []byte("test")
|
||||
|
||||
result := WriteFile(testPath, 0644)(testData)()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("writes empty file", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "empty2.txt")
|
||||
testData := []byte{}
|
||||
|
||||
result := WriteFile(testPath, 0644)(testData)()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify file is empty
|
||||
content, err := os.ReadFile(testPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, len(content))
|
||||
})
|
||||
}
|
||||
50
v2/ioeither/filter.go
Normal file
50
v2/ioeither/filter.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// 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 ioeither
|
||||
|
||||
import "github.com/IBM/fp-go/v2/either"
|
||||
|
||||
// FilterOrElse filters an IOEither value based on a predicate.
|
||||
// If the predicate returns true for the Right value, it passes through unchanged.
|
||||
// If the predicate returns false, it transforms the Right value into a Left using onFalse.
|
||||
// Left values are passed through unchanged.
|
||||
//
|
||||
// This is useful for adding validation or constraints to successful computations,
|
||||
// converting values that don't meet certain criteria into errors.
|
||||
//
|
||||
// Parameters:
|
||||
// - pred: A predicate function that tests the Right value
|
||||
// - onFalse: A function that converts the failing value into an error of type E
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that filters IOEither values based on the predicate
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Validate that a number is positive
|
||||
// isPositive := N.MoreThan(0)
|
||||
// onNegative := S.Format[int]("%d is not positive")
|
||||
//
|
||||
// validatePositive := ioeither.FilterOrElse(isPositive, onNegative)
|
||||
//
|
||||
// result1 := validatePositive(ioeither.Right[string](42))() // Right(42)
|
||||
// result2 := validatePositive(ioeither.Right[string](-5))() // Left("-5 is not positive")
|
||||
// result3 := validatePositive(ioeither.Left[int]("error"))() // Left("error")
|
||||
//
|
||||
//go:inline
|
||||
func FilterOrElse[E, A any](pred Predicate[A], onFalse func(A) E) Operator[E, A, A] {
|
||||
return ChainEitherK(either.FromPredicate(pred, onFalse))
|
||||
}
|
||||
221
v2/ioeither/filter_test.go
Normal file
221
v2/ioeither/filter_test.go
Normal file
@@ -0,0 +1,221 @@
|
||||
// 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 ioeither
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFilterOrElse_PredicateTrue(t *testing.T) {
|
||||
// Test that when predicate returns true, Right value passes through
|
||||
isPositive := N.MoreThan(0)
|
||||
onFalse := S.Format[int]("%d is not positive")
|
||||
|
||||
filter := FilterOrElse(isPositive, onFalse)
|
||||
result := filter(Right[string](42))()
|
||||
|
||||
assert.Equal(t, E.Right[string](42), result)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_PredicateFalse(t *testing.T) {
|
||||
// Test that when predicate returns false, Right value becomes Left
|
||||
isPositive := N.MoreThan(0)
|
||||
onFalse := S.Format[int]("%d is not positive")
|
||||
|
||||
filter := FilterOrElse(isPositive, onFalse)
|
||||
result := filter(Right[string](-5))()
|
||||
|
||||
assert.Equal(t, E.Left[int]("-5 is not positive"), result)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_LeftPassesThrough(t *testing.T) {
|
||||
// Test that Left values pass through unchanged
|
||||
isPositive := N.MoreThan(0)
|
||||
onFalse := S.Format[int]("%d is not positive")
|
||||
|
||||
filter := FilterOrElse(isPositive, onFalse)
|
||||
result := filter(Left[int]("original error"))()
|
||||
|
||||
assert.Equal(t, E.Left[int]("original error"), result)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_ZeroValue(t *testing.T) {
|
||||
// Test filtering with zero value
|
||||
isNonZero := func(n int) bool { return n != 0 }
|
||||
onZero := func(n int) string { return "value is zero" }
|
||||
|
||||
filter := FilterOrElse(isNonZero, onZero)
|
||||
result := filter(Right[string](0))()
|
||||
|
||||
assert.Equal(t, E.Left[int]("value is zero"), result)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_StringValidation(t *testing.T) {
|
||||
// Test with string validation
|
||||
isNonEmpty := S.IsNonEmpty
|
||||
onEmpty := func(s string) error { return fmt.Errorf("string is empty") }
|
||||
|
||||
filter := FilterOrElse(isNonEmpty, onEmpty)
|
||||
|
||||
// Non-empty string passes
|
||||
result1 := filter(Right[error]("hello"))()
|
||||
assert.Equal(t, E.Right[error]("hello"), result1)
|
||||
|
||||
// Empty string becomes error
|
||||
result2 := filter(Right[error](""))()
|
||||
assert.True(t, E.IsLeft(result2))
|
||||
assert.Equal(t, "string is empty", E.ToError(result2).Error())
|
||||
}
|
||||
|
||||
func TestFilterOrElse_ComplexPredicate(t *testing.T) {
|
||||
// Test with more complex predicate
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
isAdult := func(u User) bool { return u.Age >= 18 }
|
||||
onMinor := func(u User) string {
|
||||
return fmt.Sprintf("%s is only %d years old", u.Name, u.Age)
|
||||
}
|
||||
|
||||
filter := FilterOrElse(isAdult, onMinor)
|
||||
|
||||
// Adult user passes
|
||||
adult := User{Name: "Alice", Age: 25}
|
||||
result1 := filter(Right[string](adult))()
|
||||
assert.Equal(t, E.Right[string](adult), result1)
|
||||
|
||||
// Minor becomes error
|
||||
minor := User{Name: "Bob", Age: 16}
|
||||
result2 := filter(Right[string](minor))()
|
||||
assert.Equal(t, E.Left[User]("Bob is only 16 years old"), result2)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_ChainedFilters(t *testing.T) {
|
||||
// Test chaining multiple filters
|
||||
isPositive := N.MoreThan(0)
|
||||
onNegative := func(n int) string { return "not positive" }
|
||||
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
onOdd := func(n int) string { return "not even" }
|
||||
|
||||
filter1 := FilterOrElse(isPositive, onNegative)
|
||||
filter2 := FilterOrElse(isEven, onOdd)
|
||||
|
||||
// Chain filters - apply filter1 first, then filter2
|
||||
result := filter2(filter1(Right[string](4)))()
|
||||
assert.Equal(t, E.Right[string](4), result)
|
||||
|
||||
// Fails first filter
|
||||
result2 := filter2(filter1(Right[string](-2)))()
|
||||
assert.Equal(t, E.Left[int]("not positive"), result2)
|
||||
|
||||
// Passes first but fails second
|
||||
result3 := filter2(filter1(Right[string](3)))()
|
||||
assert.Equal(t, E.Left[int]("not even"), result3)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_WithMap(t *testing.T) {
|
||||
// Test FilterOrElse combined with Map
|
||||
isPositive := N.MoreThan(0)
|
||||
onNegative := func(n int) string { return "negative number" }
|
||||
|
||||
filter := FilterOrElse(isPositive, onNegative)
|
||||
double := Map[string](func(n int) int { return n * 2 })
|
||||
|
||||
// Compose: filter then double
|
||||
result1 := double(filter(Right[string](5)))()
|
||||
assert.Equal(t, E.Right[string](10), result1)
|
||||
|
||||
// Negative value filtered out
|
||||
result2 := double(filter(Right[string](-3)))()
|
||||
assert.Equal(t, E.Left[int]("negative number"), result2)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_BoundaryConditions(t *testing.T) {
|
||||
// Test boundary conditions
|
||||
isInRange := func(n int) bool { return n >= 0 && n <= 100 }
|
||||
onOutOfRange := func(n int) string {
|
||||
return fmt.Sprintf("%d is out of range [0, 100]", n)
|
||||
}
|
||||
|
||||
filter := FilterOrElse(isInRange, onOutOfRange)
|
||||
|
||||
// Lower boundary
|
||||
result1 := filter(Right[string](0))()
|
||||
assert.Equal(t, E.Right[string](0), result1)
|
||||
|
||||
// Upper boundary
|
||||
result2 := filter(Right[string](100))()
|
||||
assert.Equal(t, E.Right[string](100), result2)
|
||||
|
||||
// Below lower boundary
|
||||
result3 := filter(Right[string](-1))()
|
||||
assert.Equal(t, E.Left[int]("-1 is out of range [0, 100]"), result3)
|
||||
|
||||
// Above upper boundary
|
||||
result4 := filter(Right[string](101))()
|
||||
assert.Equal(t, E.Left[int]("101 is out of range [0, 100]"), result4)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_AlwaysTrue(t *testing.T) {
|
||||
// Test with predicate that always returns true
|
||||
alwaysTrue := func(n int) bool { return true }
|
||||
onFalse := func(n int) string { return "never happens" }
|
||||
|
||||
filter := FilterOrElse(alwaysTrue, onFalse)
|
||||
|
||||
result1 := filter(Right[string](42))()
|
||||
assert.Equal(t, E.Right[string](42), result1)
|
||||
|
||||
result2 := filter(Right[string](-42))()
|
||||
assert.Equal(t, E.Right[string](-42), result2)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_AlwaysFalse(t *testing.T) {
|
||||
// Test with predicate that always returns false
|
||||
alwaysFalse := func(n int) bool { return false }
|
||||
onFalse := S.Format[int]("rejected: %d")
|
||||
|
||||
filter := FilterOrElse(alwaysFalse, onFalse)
|
||||
|
||||
result := filter(Right[string](42))()
|
||||
assert.Equal(t, E.Left[int]("rejected: 42"), result)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_NilPointerValidation(t *testing.T) {
|
||||
// Test filtering nil pointers
|
||||
isNonNil := func(p *int) bool { return p != nil }
|
||||
onNil := func(p *int) string { return "pointer is nil" }
|
||||
|
||||
filter := FilterOrElse(isNonNil, onNil)
|
||||
|
||||
// Non-nil pointer passes
|
||||
value := 42
|
||||
result1 := filter(Right[string](&value))()
|
||||
assert.True(t, E.IsRight(result1))
|
||||
|
||||
// Nil pointer becomes error
|
||||
result2 := filter(Right[string]((*int)(nil)))()
|
||||
assert.Equal(t, E.Left[*int]("pointer is nil"), result2)
|
||||
}
|
||||
@@ -45,30 +45,38 @@ type (
|
||||
Operator[E, A, B any] = Kleisli[E, IOEither[E, A], B]
|
||||
)
|
||||
|
||||
// Left constructs an [IOEither] that represents a failure with an error value of type E
|
||||
func Left[A, E any](l E) IOEither[E, A] {
|
||||
return eithert.Left(io.MonadOf[Either[E, A]], l)
|
||||
}
|
||||
|
||||
// Right constructs an [IOEither] that represents a successful computation with a value of type A
|
||||
func Right[E, A any](r A) IOEither[E, A] {
|
||||
return eithert.Right(io.MonadOf[Either[E, A]], r)
|
||||
}
|
||||
|
||||
// Of constructs an [IOEither] that represents a successful computation with a value of type A.
|
||||
// This is an alias for [Right] and is the canonical way to lift a pure value into the IOEither context.
|
||||
func Of[E, A any](r A) IOEither[E, A] {
|
||||
return Right[E](r)
|
||||
}
|
||||
|
||||
// MonadOf is an alias for [Of], provided for consistency with monad naming conventions
|
||||
func MonadOf[E, A any](r A) IOEither[E, A] {
|
||||
return Of[E](r)
|
||||
}
|
||||
|
||||
// LeftIO constructs an [IOEither] from an [IO] that produces an error value
|
||||
func LeftIO[A, E any](ml IO[E]) IOEither[E, A] {
|
||||
return eithert.LeftF(io.MonadMap[E, Either[E, A]], ml)
|
||||
}
|
||||
|
||||
// RightIO constructs an [IOEither] from an [IO] that produces a success value
|
||||
func RightIO[E, A any](mr IO[A]) IOEither[E, A] {
|
||||
return eithert.RightF(io.MonadMap[A, Either[E, A]], mr)
|
||||
}
|
||||
|
||||
// FromEither lifts an [Either] value into the [IOEither] context
|
||||
func FromEither[E, A any](e Either[E, A]) IOEither[E, A] {
|
||||
return io.Of(e)
|
||||
}
|
||||
@@ -123,26 +131,32 @@ func FromLazy[E, A any](mr lazy.Lazy[A]) IOEither[E, A] {
|
||||
return FromIO[E](mr)
|
||||
}
|
||||
|
||||
// MonadMap applies a function to the value inside a successful [IOEither], leaving errors unchanged
|
||||
func MonadMap[E, A, B any](fa IOEither[E, A], f func(A) B) IOEither[E, B] {
|
||||
return eithert.MonadMap(io.MonadMap[Either[E, A], Either[E, B]], fa, f)
|
||||
}
|
||||
|
||||
// Map returns a function that applies a transformation to the value inside a successful [IOEither]
|
||||
func Map[E, A, B any](f func(A) B) Operator[E, A, B] {
|
||||
return eithert.Map(io.Map[Either[E, A], Either[E, B]], f)
|
||||
}
|
||||
|
||||
// MonadMapTo replaces the value inside a successful [IOEither] with a constant value
|
||||
func MonadMapTo[E, A, B any](fa IOEither[E, A], b B) IOEither[E, B] {
|
||||
return MonadMap(fa, function.Constant1[A](b))
|
||||
}
|
||||
|
||||
// MapTo returns a function that replaces the value inside a successful [IOEither] with a constant value
|
||||
func MapTo[E, A, B any](b B) Operator[E, A, B] {
|
||||
return Map[E](function.Constant1[A](b))
|
||||
}
|
||||
|
||||
// MonadChain sequences two [IOEither] computations, where the second depends on the result of the first
|
||||
func MonadChain[E, A, B any](fa IOEither[E, A], f Kleisli[E, A, B]) IOEither[E, B] {
|
||||
return eithert.MonadChain(io.MonadChain[Either[E, A], Either[E, B]], io.MonadOf[Either[E, B]], fa, f)
|
||||
}
|
||||
|
||||
// Chain returns a function that sequences two [IOEither] computations
|
||||
func Chain[E, A, B any](f Kleisli[E, A, B]) Operator[E, A, B] {
|
||||
return eithert.Chain(io.Chain[Either[E, A], Either[E, B]], io.Of[Either[E, B]], f)
|
||||
}
|
||||
@@ -164,6 +178,7 @@ func ChainEitherK[E, A, B any](f either.Kleisli[E, A, B]) Operator[E, A, B] {
|
||||
)
|
||||
}
|
||||
|
||||
// MonadAp applies a function wrapped in an [IOEither] to a value wrapped in an [IOEither]
|
||||
func MonadAp[B, E, A any](mab IOEither[E, func(A) B], ma IOEither[E, A]) IOEither[E, B] {
|
||||
return eithert.MonadAp(
|
||||
io.MonadAp[Either[E, A], Either[E, B]],
|
||||
@@ -171,7 +186,8 @@ func MonadAp[B, E, A any](mab IOEither[E, func(A) B], ma IOEither[E, A]) IOEithe
|
||||
mab, ma)
|
||||
}
|
||||
|
||||
// Ap is an alias of [ApPar]
|
||||
// Ap applies a function wrapped in an [IOEither] to a value wrapped in an [IOEither].
|
||||
// This is an alias of [ApPar] which applies the function and value in parallel.
|
||||
func Ap[B, E, A any](ma IOEither[E, A]) Operator[E, func(A) B, B] {
|
||||
return eithert.Ap(
|
||||
io.Ap[Either[E, B], Either[E, A]],
|
||||
@@ -179,6 +195,7 @@ func Ap[B, E, A any](ma IOEither[E, A]) Operator[E, func(A) B, B] {
|
||||
ma)
|
||||
}
|
||||
|
||||
// MonadApPar applies a function wrapped in an [IOEither] to a value wrapped in an [IOEither] in parallel
|
||||
func MonadApPar[B, E, A any](mab IOEither[E, func(A) B], ma IOEither[E, A]) IOEither[E, B] {
|
||||
return eithert.MonadAp(
|
||||
io.MonadApPar[Either[E, A], Either[E, B]],
|
||||
@@ -186,7 +203,7 @@ func MonadApPar[B, E, A any](mab IOEither[E, func(A) B], ma IOEither[E, A]) IOEi
|
||||
mab, ma)
|
||||
}
|
||||
|
||||
// ApPar applies function and value in parallel
|
||||
// ApPar applies a function wrapped in an [IOEither] to a value wrapped in an [IOEither] in parallel
|
||||
func ApPar[B, E, A any](ma IOEither[E, A]) Operator[E, func(A) B, B] {
|
||||
return eithert.Ap(
|
||||
io.ApPar[Either[E, B], Either[E, A]],
|
||||
@@ -194,6 +211,7 @@ func ApPar[B, E, A any](ma IOEither[E, A]) Operator[E, func(A) B, B] {
|
||||
ma)
|
||||
}
|
||||
|
||||
// MonadApSeq applies a function wrapped in an [IOEither] to a value wrapped in an [IOEither] sequentially
|
||||
func MonadApSeq[B, E, A any](mab IOEither[E, func(A) B], ma IOEither[E, A]) IOEither[E, B] {
|
||||
return eithert.MonadAp(
|
||||
io.MonadApSeq[Either[E, A], Either[E, B]],
|
||||
@@ -201,7 +219,7 @@ func MonadApSeq[B, E, A any](mab IOEither[E, func(A) B], ma IOEither[E, A]) IOEi
|
||||
mab, ma)
|
||||
}
|
||||
|
||||
// ApSeq applies function and value sequentially
|
||||
// ApSeq applies a function wrapped in an [IOEither] to a value wrapped in an [IOEither] sequentially
|
||||
func ApSeq[B, E, A any](ma IOEither[E, A]) func(IOEither[E, func(A) B]) IOEither[E, B] {
|
||||
return eithert.Ap(
|
||||
io.ApSeq[Either[E, B], Either[E, A]],
|
||||
@@ -209,10 +227,12 @@ func ApSeq[B, E, A any](ma IOEither[E, A]) func(IOEither[E, func(A) B]) IOEither
|
||||
ma)
|
||||
}
|
||||
|
||||
// Flatten removes one level of nesting from a nested [IOEither]
|
||||
func Flatten[E, A any](mma IOEither[E, IOEither[E, A]]) IOEither[E, A] {
|
||||
return MonadChain(mma, function.Identity[IOEither[E, A]])
|
||||
}
|
||||
|
||||
// TryCatch executes a function that may throw an error and converts it to an [IOEither]
|
||||
func TryCatch[E, A any](f func() (A, error), onThrow func(error) E) IOEither[E, A] {
|
||||
return func() Either[E, A] {
|
||||
a, err := f()
|
||||
@@ -220,16 +240,19 @@ func TryCatch[E, A any](f func() (A, error), onThrow func(error) E) IOEither[E,
|
||||
}
|
||||
}
|
||||
|
||||
// TryCatchError executes a function that may throw an error and converts it to an [IOEither] with error type error
|
||||
func TryCatchError[A any](f func() (A, error)) IOEither[error, A] {
|
||||
return func() Either[error, A] {
|
||||
return either.TryCatchError(f())
|
||||
}
|
||||
}
|
||||
|
||||
// Memoize caches the result of an [IOEither] computation so it's only executed once
|
||||
func Memoize[E, A any](ma IOEither[E, A]) IOEither[E, A] {
|
||||
return io.Memoize(ma)
|
||||
}
|
||||
|
||||
// MonadMapLeft applies a function to the error value of a failed [IOEither], leaving successful values unchanged
|
||||
func MonadMapLeft[A, E1, E2 any](fa IOEither[E1, A], f func(E1) E2) IOEither[E2, A] {
|
||||
return eithert.MonadMapLeft(
|
||||
io.MonadMap[Either[E1, A], Either[E2, A]],
|
||||
@@ -238,6 +261,7 @@ func MonadMapLeft[A, E1, E2 any](fa IOEither[E1, A], f func(E1) E2) IOEither[E2,
|
||||
)
|
||||
}
|
||||
|
||||
// MapLeft returns a function that applies a transformation to the error value of a failed [IOEither]
|
||||
func MapLeft[A, E1, E2 any](f func(E1) E2) func(IOEither[E1, A]) IOEither[E2, A] {
|
||||
return eithert.MapLeft(
|
||||
io.Map[Either[E1, A], Either[E2, A]],
|
||||
@@ -245,51 +269,52 @@ func MapLeft[A, E1, E2 any](f func(E1) E2) func(IOEither[E1, A]) IOEither[E2, A]
|
||||
)
|
||||
}
|
||||
|
||||
// MonadBiMap applies one function to the error value and another to the success value of an [IOEither]
|
||||
func MonadBiMap[E1, E2, A, B any](fa IOEither[E1, A], f func(E1) E2, g func(A) B) IOEither[E2, B] {
|
||||
return eithert.MonadBiMap(io.MonadMap[Either[E1, A], Either[E2, B]], fa, f, g)
|
||||
}
|
||||
|
||||
// BiMap maps a pair of functions over the two type arguments of the bifunctor.
|
||||
// BiMap returns a function that maps a pair of functions over the two type arguments of the bifunctor
|
||||
func BiMap[E1, E2, A, B any](f func(E1) E2, g func(A) B) func(IOEither[E1, A]) IOEither[E2, B] {
|
||||
return eithert.BiMap(io.Map[Either[E1, A], Either[E2, B]], f, g)
|
||||
}
|
||||
|
||||
// Fold converts an IOEither into an IO
|
||||
// Fold converts an [IOEither] into an [IO] by providing handlers for both the error and success cases
|
||||
func Fold[E, A, B any](onLeft func(E) IO[B], onRight io.Kleisli[A, B]) func(IOEither[E, A]) IO[B] {
|
||||
return eithert.MatchE(io.MonadChain[Either[E, A], B], onLeft, onRight)
|
||||
}
|
||||
|
||||
// GetOrElse extracts the value or maps the error
|
||||
// GetOrElse extracts the value from a successful [IOEither] or computes a default value from the error
|
||||
func GetOrElse[E, A any](onLeft func(E) IO[A]) func(IOEither[E, A]) IO[A] {
|
||||
return eithert.GetOrElse(io.MonadChain[Either[E, A], A], io.MonadOf[A], onLeft)
|
||||
}
|
||||
|
||||
// GetOrElseOf extracts the value or maps the error
|
||||
// GetOrElseOf extracts the value from a successful [IOEither] or computes a default value from the error
|
||||
func GetOrElseOf[E, A any](onLeft func(E) A) func(IOEither[E, A]) IO[A] {
|
||||
return eithert.GetOrElseOf(io.MonadChain[Either[E, A], A], io.MonadOf[A], onLeft)
|
||||
}
|
||||
|
||||
// MonadChainTo composes to the second monad ignoring the return value of the first
|
||||
// MonadChainTo sequences two [IOEither] computations, discarding the result of the first
|
||||
func MonadChainTo[A, E, B any](fa IOEither[E, A], fb IOEither[E, B]) IOEither[E, B] {
|
||||
return MonadChain(fa, function.Constant1[A](fb))
|
||||
}
|
||||
|
||||
// ChainTo composes to the second [IOEither] monad ignoring the return value of the first
|
||||
// ChainTo returns a function that sequences two [IOEither] computations, discarding the result of the first
|
||||
func ChainTo[A, E, B any](fb IOEither[E, B]) Operator[E, A, B] {
|
||||
return Chain(function.Constant1[A](fb))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
// MonadChainToIO sequences an [IOEither] with an [IO], discarding the result of the first
|
||||
func MonadChainToIO[E, A, B any](fa IOEither[E, A], fb IO[B]) IOEither[E, B] {
|
||||
return MonadChainTo(fa, FromIO[E](fb))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
// ChainToIO returns a function that sequences an [IOEither] with an [IO], discarding the result of the first
|
||||
func ChainToIO[E, A, B any](fb IO[B]) Operator[E, A, B] {
|
||||
return ChainTo[A](FromIO[E](fb))
|
||||
}
|
||||
|
||||
// MonadChainFirst runs the [IOEither] monad returned by the function but returns the result of the original monad
|
||||
// MonadChainFirst executes a side-effecting [IOEither] computation but returns the original value
|
||||
func MonadChainFirst[E, A, B any](ma IOEither[E, A], f Kleisli[E, A, B]) IOEither[E, A] {
|
||||
return chain.MonadChainFirst(
|
||||
MonadChain[E, A, A],
|
||||
@@ -299,12 +324,12 @@ func MonadChainFirst[E, A, B any](ma IOEither[E, A], f Kleisli[E, A, B]) IOEithe
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
// MonadTap is an alias for [MonadChainFirst], executing a side effect while preserving the original value
|
||||
func MonadTap[E, A, B any](ma IOEither[E, A], f Kleisli[E, A, B]) IOEither[E, A] {
|
||||
return MonadChainFirst(ma, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
// ChainFirst returns a function that executes a side-effecting [IOEither] computation but returns the original value
|
||||
func ChainFirst[E, A, B any](f Kleisli[E, A, B]) Operator[E, A, A] {
|
||||
return chain.ChainFirst(
|
||||
Chain[E, A, A],
|
||||
@@ -313,11 +338,12 @@ func ChainFirst[E, A, B any](f Kleisli[E, A, B]) Operator[E, A, A] {
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
// Tap is an alias for [ChainFirst], executing a side effect while preserving the original value
|
||||
func Tap[E, A, B any](f Kleisli[E, A, B]) Operator[E, A, A] {
|
||||
return ChainFirst(f)
|
||||
}
|
||||
|
||||
// MonadChainFirstEitherK executes a side-effecting [Either] computation but returns the original [IOEither] value
|
||||
func MonadChainFirstEitherK[A, E, B any](ma IOEither[E, A], f either.Kleisli[E, A, B]) IOEither[E, A] {
|
||||
return fromeither.MonadChainFirstEitherK(
|
||||
MonadChain[E, A, A],
|
||||
@@ -328,6 +354,7 @@ func MonadChainFirstEitherK[A, E, B any](ma IOEither[E, A], f either.Kleisli[E,
|
||||
)
|
||||
}
|
||||
|
||||
// ChainFirstEitherK returns a function that executes a side-effecting [Either] computation but returns the original value
|
||||
func ChainFirstEitherK[A, E, B any](f either.Kleisli[E, A, B]) Operator[E, A, A] {
|
||||
return fromeither.ChainFirstEitherK(
|
||||
Chain[E, A, A],
|
||||
@@ -337,7 +364,7 @@ func ChainFirstEitherK[A, E, B any](f either.Kleisli[E, A, B]) Operator[E, A, A]
|
||||
)
|
||||
}
|
||||
|
||||
// MonadChainFirstIOK runs [IO] the monad returned by the function but returns the result of the original monad
|
||||
// MonadChainFirstIOK executes a side-effecting [IO] computation but returns the original [IOEither] value
|
||||
func MonadChainFirstIOK[E, A, B any](ma IOEither[E, A], f io.Kleisli[A, B]) IOEither[E, A] {
|
||||
return fromio.MonadChainFirstIOK(
|
||||
MonadChain[E, A, A],
|
||||
@@ -348,7 +375,7 @@ func MonadChainFirstIOK[E, A, B any](ma IOEither[E, A], f io.Kleisli[A, B]) IOEi
|
||||
)
|
||||
}
|
||||
|
||||
// ChainFirstIOK runs the [IO] monad returned by the function but returns the result of the original monad
|
||||
// ChainFirstIOK returns a function that executes a side-effecting [IO] computation but returns the original value
|
||||
func ChainFirstIOK[E, A, B any](f io.Kleisli[A, B]) Operator[E, A, A] {
|
||||
return fromio.ChainFirstIOK(
|
||||
Chain[E, A, A],
|
||||
@@ -358,31 +385,33 @@ func ChainFirstIOK[E, A, B any](f io.Kleisli[A, B]) Operator[E, A, A] {
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
// MonadTapEitherK is an alias for [MonadChainFirstEitherK], executing an [Either] side effect while preserving the original value
|
||||
func MonadTapEitherK[A, E, B any](ma IOEither[E, A], f either.Kleisli[E, A, B]) IOEither[E, A] {
|
||||
return MonadChainFirstEitherK(ma, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
// TapEitherK is an alias for [ChainFirstEitherK], executing an [Either] side effect while preserving the original value
|
||||
func TapEitherK[A, E, B any](f either.Kleisli[E, A, B]) Operator[E, A, A] {
|
||||
return ChainFirstEitherK(f)
|
||||
}
|
||||
|
||||
// MonadChainFirstIOK runs [IO] the monad returned by the function but returns the result of the original monad
|
||||
// MonadTapIOK is an alias for [MonadChainFirstIOK], executing an [IO] side effect while preserving the original value
|
||||
func MonadTapIOK[E, A, B any](ma IOEither[E, A], f io.Kleisli[A, B]) IOEither[E, A] {
|
||||
return MonadChainFirstIOK(ma, f)
|
||||
}
|
||||
|
||||
// ChainFirstIOK runs the [IO] monad returned by the function but returns the result of the original monad
|
||||
// TapIOK is an alias for [ChainFirstIOK], executing an [IO] side effect while preserving the original value
|
||||
func TapIOK[E, A, B any](f io.Kleisli[A, B]) Operator[E, A, A] {
|
||||
return ChainFirstIOK[E](f)
|
||||
}
|
||||
|
||||
// MonadFold eliminates an [IOEither] by providing handlers for both error and success cases, returning an [IO]
|
||||
func MonadFold[E, A, B any](ma IOEither[E, A], onLeft func(E) IO[B], onRight io.Kleisli[A, B]) IO[B] {
|
||||
return eithert.FoldE(io.MonadChain[Either[E, A], B], ma, onLeft, onRight)
|
||||
}
|
||||
|
||||
// WithResource constructs a function that creates a resource, then operates on it and then releases the resource
|
||||
// WithResource constructs a function that safely manages a resource with automatic cleanup.
|
||||
// It creates a resource, operates on it, and ensures the resource is released even if an error occurs.
|
||||
func WithResource[A, E, R, ANY any](onCreate IOEither[E, R], onRelease Kleisli[E, R, ANY]) Kleisli[E, Kleisli[E, R, A], A] {
|
||||
return file.WithResource(
|
||||
MonadChain[E, R, A],
|
||||
@@ -393,12 +422,12 @@ func WithResource[A, E, R, ANY any](onCreate IOEither[E, R], onRelease Kleisli[E
|
||||
)(function.Constant(onCreate), onRelease)
|
||||
}
|
||||
|
||||
// Swap changes the order of type parameters
|
||||
// Swap exchanges the error and success type parameters of an [IOEither]
|
||||
func Swap[E, A any](val IOEither[E, A]) IOEither[A, E] {
|
||||
return MonadFold(val, Right[A, E], Left[E, A])
|
||||
}
|
||||
|
||||
// FromImpure converts a side effect without a return value into a side effect that returns any
|
||||
// FromImpure converts a side effect without a return value into an [IOEither] that returns any
|
||||
func FromImpure[E any](f func()) IOEither[E, any] {
|
||||
return function.Pipe2(
|
||||
f,
|
||||
@@ -407,12 +436,12 @@ func FromImpure[E any](f func()) IOEither[E, any] {
|
||||
)
|
||||
}
|
||||
|
||||
// Defer creates an IO by creating a brand new IO via a generator function, each time
|
||||
// Defer creates an [IOEither] by lazily evaluating a generator function each time the [IOEither] is executed
|
||||
func Defer[E, A any](gen lazy.Lazy[IOEither[E, A]]) IOEither[E, A] {
|
||||
return io.Defer(gen)
|
||||
}
|
||||
|
||||
// MonadAlt identifies an associative operation on a type constructor
|
||||
// MonadAlt provides an alternative [IOEither] computation if the first one fails
|
||||
func MonadAlt[E, A any](first IOEither[E, A], second lazy.Lazy[IOEither[E, A]]) IOEither[E, A] {
|
||||
return eithert.MonadAlt(
|
||||
io.Of[Either[E, A]],
|
||||
@@ -423,20 +452,22 @@ func MonadAlt[E, A any](first IOEither[E, A], second lazy.Lazy[IOEither[E, A]])
|
||||
)
|
||||
}
|
||||
|
||||
// Alt identifies an associative operation on a type constructor
|
||||
// Alt returns a function that provides an alternative [IOEither] computation if the first one fails
|
||||
func Alt[E, A any](second lazy.Lazy[IOEither[E, A]]) Operator[E, A, A] {
|
||||
return function.Bind2nd(MonadAlt[E, A], second)
|
||||
}
|
||||
|
||||
// MonadFlap applies a value to a function wrapped in an [IOEither]
|
||||
func MonadFlap[E, B, A any](fab IOEither[E, func(A) B], a A) IOEither[E, B] {
|
||||
return functor.MonadFlap(MonadMap[E, func(A) B, B], fab, a)
|
||||
}
|
||||
|
||||
// Flap returns a function that applies a value to a function wrapped in an [IOEither]
|
||||
func Flap[E, B, A any](a A) Operator[E, func(A) B, B] {
|
||||
return functor.Flap(Map[E, func(A) B, B], a)
|
||||
}
|
||||
|
||||
// ToIOOption converts an [IOEither] to an [IOO.IOOption]
|
||||
// ToIOOption converts an [IOEither] to an [IOO.IOOption], discarding error information
|
||||
func ToIOOption[E, A any](ioe IOEither[E, A]) IOO.IOOption[A] {
|
||||
return function.Pipe1(
|
||||
ioe,
|
||||
@@ -444,12 +475,12 @@ func ToIOOption[E, A any](ioe IOEither[E, A]) IOO.IOOption[A] {
|
||||
)
|
||||
}
|
||||
|
||||
// Delay creates an operation that passes in the value after some delay
|
||||
// Delay creates an operator that delays the execution of an [IOEither] by the specified duration
|
||||
func Delay[E, A any](delay time.Duration) Operator[E, A, A] {
|
||||
return io.Delay[Either[E, A]](delay)
|
||||
}
|
||||
|
||||
// After creates an operation that passes after the given [time.Time]
|
||||
// After creates an operator that delays the execution of an [IOEither] until the specified time
|
||||
func After[E, A any](timestamp time.Time) Operator[E, A, A] {
|
||||
return io.After[Either[E, A]](timestamp)
|
||||
}
|
||||
@@ -552,7 +583,13 @@ func ChainLeft[EA, EB, A any](f Kleisli[EB, EA, A]) func(IOEither[EA, A]) IOEith
|
||||
// )
|
||||
// // result will always be Left("database error"), even though f returns Right
|
||||
func MonadChainFirstLeft[A, EA, EB, B any](ma IOEither[EA, A], f Kleisli[EB, EA, B]) IOEither[EA, A] {
|
||||
return MonadChainLeft(ma, function.Flow2(f, Fold(function.Constant1[EB](ma), function.Constant1[B](ma))))
|
||||
return eithert.MonadChainFirstLeft(
|
||||
io.MonadChain[Either[EA, A], Either[EA, A]],
|
||||
io.MonadMap[Either[EB, B], Either[EA, A]],
|
||||
io.MonadOf[Either[EA, A]],
|
||||
ma,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
@@ -589,13 +626,40 @@ func MonadTapLeft[A, EA, EB, B any](ma IOEither[EA, A], f Kleisli[EB, EA, B]) IO
|
||||
// )
|
||||
// // result is always Left("validation failed"), even though f returns Right
|
||||
func ChainFirstLeft[A, EA, EB, B any](f Kleisli[EB, EA, B]) Operator[EA, A, A] {
|
||||
return ChainLeft(func(e EA) IOEither[EA, A] {
|
||||
ma := Left[A](e)
|
||||
return MonadFold(f(e), function.Constant1[EB](ma), function.Constant1[B](ma))
|
||||
})
|
||||
return eithert.ChainFirstLeft(
|
||||
io.Chain[Either[EA, A], Either[EA, A]],
|
||||
io.Map[Either[EB, B], Either[EA, A]],
|
||||
io.Of[Either[EA, A]],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TapLeft[A, EA, EB, B any](f Kleisli[EB, EA, B]) Operator[EA, A, A] {
|
||||
return ChainFirstLeft[A](f)
|
||||
}
|
||||
|
||||
// OrElse recovers from a Left (error) by providing an alternative computation.
|
||||
// If the IOEither is Right, it returns the value unchanged.
|
||||
// If the IOEither is Left, it applies the provided function to the error value,
|
||||
// which returns a new IOEither that replaces the original.
|
||||
//
|
||||
// This is useful for error recovery, fallback logic, or chaining alternative IO computations.
|
||||
// The error type can be widened from E1 to E2, allowing transformation of error types.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Recover from specific errors with fallback IO operations
|
||||
// recover := ioeither.OrElse(func(err error) ioeither.IOEither[error, int] {
|
||||
// if err.Error() == "not found" {
|
||||
// return ioeither.Right[error](0) // default value
|
||||
// }
|
||||
// return ioeither.Left[int](err) // propagate other errors
|
||||
// })
|
||||
// result := recover(ioeither.Left[int](errors.New("not found"))) // Right(0)
|
||||
// result := recover(ioeither.Right[error](42)) // Right(42) - unchanged
|
||||
//
|
||||
//go:inline
|
||||
func OrElse[E1, E2, A any](onLeft Kleisli[E2, E1, A]) Kleisli[E2, IOEither[E1, A], A] {
|
||||
return Fold(onLeft, Of[E2, A])
|
||||
}
|
||||
|
||||
@@ -401,3 +401,92 @@ func TestChainFirstLeft(t *testing.T) {
|
||||
assert.Equal(t, E.Left[string]("step2"), actualResult)
|
||||
})
|
||||
}
|
||||
|
||||
func TestOrElse(t *testing.T) {
|
||||
// Test basic recovery from Left
|
||||
recover := OrElse(func(e error) IOEither[error, int] {
|
||||
return Right[error](0)
|
||||
})
|
||||
|
||||
result := recover(Left[int](fmt.Errorf("error")))()
|
||||
assert.Equal(t, E.Right[error](0), result)
|
||||
|
||||
// Test Right value passes through unchanged
|
||||
result = recover(Right[error](42))()
|
||||
assert.Equal(t, E.Right[error](42), result)
|
||||
|
||||
// Test selective recovery - recover some errors, propagate others
|
||||
selectiveRecover := OrElse(func(err error) IOEither[error, int] {
|
||||
if err.Error() == "not found" {
|
||||
return Right[error](0) // default value for "not found"
|
||||
}
|
||||
return Left[int](err) // propagate other errors
|
||||
})
|
||||
assert.Equal(t, E.Right[error](0), selectiveRecover(Left[int](fmt.Errorf("not found")))())
|
||||
permissionErr := fmt.Errorf("permission denied")
|
||||
assert.Equal(t, E.Left[int](permissionErr), selectiveRecover(Left[int](permissionErr))())
|
||||
|
||||
// Test chaining multiple OrElse operations
|
||||
firstRecover := OrElse(func(err error) IOEither[error, int] {
|
||||
if err.Error() == "error1" {
|
||||
return Right[error](1)
|
||||
}
|
||||
return Left[int](err)
|
||||
})
|
||||
secondRecover := OrElse(func(err error) IOEither[error, int] {
|
||||
if err.Error() == "error2" {
|
||||
return Right[error](2)
|
||||
}
|
||||
return Left[int](err)
|
||||
})
|
||||
assert.Equal(t, E.Right[error](1), F.Pipe1(Left[int](fmt.Errorf("error1")), firstRecover)())
|
||||
assert.Equal(t, E.Right[error](2), F.Pipe1(Left[int](fmt.Errorf("error2")), F.Flow2(firstRecover, secondRecover))())
|
||||
}
|
||||
|
||||
func TestOrElseW(t *testing.T) {
|
||||
type ValidationError string
|
||||
type AppError int
|
||||
|
||||
// Test with Right value - should return Right with widened error type
|
||||
rightValue := Right[ValidationError]("success")
|
||||
recoverValidation := OrElse(func(ve ValidationError) IOEither[AppError, string] {
|
||||
return Left[string](AppError(400))
|
||||
})
|
||||
result := recoverValidation(rightValue)()
|
||||
assert.True(t, E.IsRight(result))
|
||||
assert.Equal(t, "success", F.Pipe1(result, E.GetOrElse(F.Constant1[AppError](""))))
|
||||
|
||||
// Test with Left value - should apply recovery with new error type
|
||||
leftValue := Left[string](ValidationError("invalid input"))
|
||||
result = recoverValidation(leftValue)()
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, leftVal := E.Unwrap(result)
|
||||
assert.Equal(t, AppError(400), leftVal)
|
||||
|
||||
// Test error type conversion - ValidationError to AppError
|
||||
convertError := OrElse(func(ve ValidationError) IOEither[AppError, int] {
|
||||
return Left[int](AppError(len(ve)))
|
||||
})
|
||||
converted := convertError(Left[int](ValidationError("short")))()
|
||||
assert.True(t, E.IsLeft(converted))
|
||||
_, leftConv := E.Unwrap(converted)
|
||||
assert.Equal(t, AppError(5), leftConv)
|
||||
|
||||
// Test recovery to Right with widened error type
|
||||
recoverToRight := OrElse(func(ve ValidationError) IOEither[AppError, int] {
|
||||
if ve == "recoverable" {
|
||||
return Right[AppError](99)
|
||||
}
|
||||
return Left[int](AppError(500))
|
||||
})
|
||||
assert.Equal(t, E.Right[AppError](99), recoverToRight(Left[int](ValidationError("recoverable")))())
|
||||
assert.True(t, E.IsLeft(recoverToRight(Left[int](ValidationError("fatal")))()))
|
||||
|
||||
// Test that Right values are preserved with widened error type
|
||||
preservedRight := Right[ValidationError](42)
|
||||
preserveRecover := OrElse(func(ve ValidationError) IOEither[AppError, int] {
|
||||
return Left[int](AppError(999))
|
||||
})
|
||||
preserved := preserveRecover(preservedRight)()
|
||||
assert.Equal(t, E.Right[AppError](42), preserved)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func ApplicativeMonoid[E, A any](
|
||||
)
|
||||
}
|
||||
|
||||
// ApplicativeMonoid returns a [Monoid] that concatenates [IOEither] instances via their applicative
|
||||
// ApplicativeMonoidSeq returns a [Monoid] that concatenates [IOEither] instances sequentially via their applicative
|
||||
func ApplicativeMonoidSeq[E, A any](
|
||||
m monoid.Monoid[A],
|
||||
) Monoid[E, A] {
|
||||
@@ -47,7 +47,7 @@ func ApplicativeMonoidSeq[E, A any](
|
||||
)
|
||||
}
|
||||
|
||||
// ApplicativeMonoid returns a [Monoid] that concatenates [IOEither] instances via their applicative
|
||||
// ApplicativeMonoidPar returns a [Monoid] that concatenates [IOEither] instances in parallel via their applicative
|
||||
func ApplicativeMonoidPar[E, A any](
|
||||
m monoid.Monoid[A],
|
||||
) Monoid[E, A] {
|
||||
|
||||
95
v2/ioeither/monoid_extended_test.go
Normal file
95
v2/ioeither/monoid_extended_test.go
Normal file
@@ -0,0 +1,95 @@
|
||||
// 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 ioeither
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestApplicativeMonoidSeq(t *testing.T) {
|
||||
m := ApplicativeMonoidSeq[error](S.Monoid)
|
||||
|
||||
t.Run("concatenates two Right values sequentially", func(t *testing.T) {
|
||||
result := m.Concat(Of[error]("hello"), Of[error](" world"))()
|
||||
assert.Equal(t, E.Of[error]("hello world"), result)
|
||||
})
|
||||
|
||||
t.Run("empty with Right value", func(t *testing.T) {
|
||||
result := m.Concat(Of[error]("test"), m.Empty())()
|
||||
assert.Equal(t, E.Of[error]("test"), result)
|
||||
})
|
||||
|
||||
t.Run("Right value with empty", func(t *testing.T) {
|
||||
result := m.Concat(m.Empty(), Of[error]("test"))()
|
||||
assert.Equal(t, E.Of[error]("test"), result)
|
||||
})
|
||||
|
||||
t.Run("Left value short-circuits", func(t *testing.T) {
|
||||
err := errors.New("error")
|
||||
result := m.Concat(Left[string](err), Of[error]("test"))()
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("Right with Left returns Left", func(t *testing.T) {
|
||||
err := errors.New("error")
|
||||
result := m.Concat(Of[error]("test"), Left[string](err))()
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeMonoidPar(t *testing.T) {
|
||||
m := ApplicativeMonoidPar[error](S.Monoid)
|
||||
|
||||
t.Run("concatenates two Right values in parallel", func(t *testing.T) {
|
||||
result := m.Concat(Of[error]("hello"), Of[error](" world"))()
|
||||
assert.Equal(t, E.Of[error]("hello world"), result)
|
||||
})
|
||||
|
||||
t.Run("empty with Right value", func(t *testing.T) {
|
||||
result := m.Concat(Of[error]("test"), m.Empty())()
|
||||
assert.Equal(t, E.Of[error]("test"), result)
|
||||
})
|
||||
|
||||
t.Run("Right value with empty", func(t *testing.T) {
|
||||
result := m.Concat(m.Empty(), Of[error]("test"))()
|
||||
assert.Equal(t, E.Of[error]("test"), result)
|
||||
})
|
||||
|
||||
t.Run("Left value returns Left", func(t *testing.T) {
|
||||
err := errors.New("error")
|
||||
result := m.Concat(Left[string](err), Of[error]("test"))()
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("Right with Left returns Left", func(t *testing.T) {
|
||||
err := errors.New("error")
|
||||
result := m.Concat(Of[error]("test"), Left[string](err))()
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("multiple concatenations", func(t *testing.T) {
|
||||
result := m.Concat(
|
||||
m.Concat(Of[error]("a"), Of[error]("b")),
|
||||
m.Concat(Of[error]("c"), Of[error]("d")),
|
||||
)()
|
||||
assert.Equal(t, E.Of[error]("abcd"), result)
|
||||
})
|
||||
}
|
||||
70
v2/ioeither/semigroup_test.go
Normal file
70
v2/ioeither/semigroup_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// 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 ioeither
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAltSemigroup(t *testing.T) {
|
||||
s := AltSemigroup[error, int]()
|
||||
|
||||
t.Run("first Right returns first", func(t *testing.T) {
|
||||
first := Of[error](1)
|
||||
second := Of[error](2)
|
||||
result := s.Concat(first, second)()
|
||||
assert.Equal(t, E.Of[error](1), result)
|
||||
})
|
||||
|
||||
t.Run("first Left tries second Right", func(t *testing.T) {
|
||||
first := Left[int](errors.New("error1"))
|
||||
second := Of[error](2)
|
||||
result := s.Concat(first, second)()
|
||||
assert.Equal(t, E.Of[error](2), result)
|
||||
})
|
||||
|
||||
t.Run("both Left returns second Left", func(t *testing.T) {
|
||||
err1 := errors.New("error1")
|
||||
err2 := errors.New("error2")
|
||||
first := Left[int](err1)
|
||||
second := Left[int](err2)
|
||||
result := s.Concat(first, second)()
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, leftVal := E.Unwrap(result)
|
||||
assert.Equal(t, err2, leftVal)
|
||||
})
|
||||
|
||||
t.Run("chaining multiple alternatives", func(t *testing.T) {
|
||||
first := Left[int](errors.New("error1"))
|
||||
second := Left[int](errors.New("error2"))
|
||||
third := Of[error](3)
|
||||
result := s.Concat(s.Concat(first, second), third)()
|
||||
assert.Equal(t, E.Of[error](3), result)
|
||||
})
|
||||
|
||||
t.Run("first Right short-circuits", func(t *testing.T) {
|
||||
first := Of[error](1)
|
||||
second := Of[error](2)
|
||||
|
||||
// When first succeeds, it returns immediately
|
||||
result := s.Concat(first, second)()
|
||||
assert.Equal(t, E.Of[error](1), result)
|
||||
})
|
||||
}
|
||||
@@ -151,7 +151,7 @@ func TraverseArrayWithIndexPar[E, A, B any](f func(int, A) IOEither[E, B]) Kleis
|
||||
)
|
||||
}
|
||||
|
||||
// SequenceArrayPar converts a homogeneous Paruence of either into an either of Paruence
|
||||
// SequenceArrayPar converts a homogeneous sequence of either into an either of sequence
|
||||
func SequenceArrayPar[E, A any](ma []IOEither[E, A]) IOEither[E, []A] {
|
||||
return TraverseArrayPar(function.Identity[IOEither[E, A]])(ma)
|
||||
}
|
||||
@@ -172,13 +172,13 @@ func TraverseRecordWithIndexPar[K comparable, E, A, B any](f func(K, A) IOEither
|
||||
return record.TraverseWithIndex[map[K]A](
|
||||
Of[E, map[K]B],
|
||||
Map[E, map[K]B, func(B) map[K]B],
|
||||
ApSeq[map[K]B, E, B],
|
||||
ApPar[map[K]B, E, B],
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// SequenceRecordPar converts a homogeneous Paruence of either into an either of Paruence
|
||||
// SequenceRecordPar converts a homogeneous sequence of either into an either of sequence
|
||||
func SequenceRecordPar[K comparable, E, A any](ma map[K]IOEither[E, A]) IOEither[E, map[K]A] {
|
||||
return TraverseRecordPar[K](function.Identity[IOEither[E, A]])(ma)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
package ioeither
|
||||
|
||||
import "github.com/IBM/fp-go/v2/consumer"
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/consumer"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
)
|
||||
|
||||
type (
|
||||
// Consumer represents a function that consumes a value of type A.
|
||||
Consumer[A any] = consumer.Consumer[A]
|
||||
|
||||
// Predicate represents a function that tests a value of type A and returns a boolean.
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
)
|
||||
|
||||
@@ -27,19 +27,37 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
// Either represents a value of one of two possible types (a disjoint union).
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
Option[A any] = option.Option[A]
|
||||
IO[A any] = io.IO[A]
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
|
||||
// IOOption represents a synchronous computation that may fail
|
||||
// refer to [https://andywhite.xyz/posts/2021-01-27-rte-foundations/#ioeitherlte-agt] for more details
|
||||
// Option represents an optional value that may or may not be present.
|
||||
Option[A any] = option.Option[A]
|
||||
|
||||
// IO represents a synchronous computation that cannot fail.
|
||||
IO[A any] = io.IO[A]
|
||||
|
||||
// Lazy represents a deferred computation that produces a value of type A.
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
|
||||
// IOOption represents a synchronous computation that may not produce a value.
|
||||
// It combines IO (side effects) with Option (optional values).
|
||||
// Refer to [https://andywhite.xyz/posts/2021-01-27-rte-foundations/#ioeitherlte-agt] for more details.
|
||||
IOOption[A any] = io.IO[Option[A]]
|
||||
|
||||
Kleisli[A, B any] = reader.Reader[A, IOOption[B]]
|
||||
Operator[A, B any] = Kleisli[IOOption[A], B]
|
||||
Consumer[A any] = consumer.Consumer[A]
|
||||
// Kleisli represents a Kleisli arrow for the IOOption monad.
|
||||
// It's a function from A to IOOption[B], used for composing operations that may not produce a value.
|
||||
Kleisli[A, B any] = reader.Reader[A, IOOption[B]]
|
||||
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
// Operator represents a function that transforms one IOOption into another.
|
||||
// It takes an IOOption[A] and produces an IOOption[B].
|
||||
Operator[A, B any] = Kleisli[IOOption[A], B]
|
||||
|
||||
// Consumer represents a function that consumes a value of type A.
|
||||
Consumer[A any] = consumer.Consumer[A]
|
||||
|
||||
// Lens is an optic that focuses on a field of type T within a structure of type S.
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
|
||||
// Prism is an optic that focuses on a case of a sum type.
|
||||
Prism[S, T any] = prism.Prism[S, T]
|
||||
)
|
||||
|
||||
@@ -90,6 +90,6 @@ func Remove(name string) IOResult[string] {
|
||||
// Close closes an object
|
||||
//
|
||||
//go:inline
|
||||
func Close[C io.Closer](c C) IOResult[any] {
|
||||
func Close[C io.Closer](c C) IOResult[struct{}] {
|
||||
return file.Close(c)
|
||||
}
|
||||
|
||||
@@ -489,3 +489,28 @@ func ChainFirstLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
|
||||
func TapLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
|
||||
return ioeither.TapLeft[A](f)
|
||||
}
|
||||
|
||||
// OrElse recovers from a Left (error) by providing an alternative computation.
|
||||
// If the IOResult is Right, it returns the value unchanged.
|
||||
// If the IOResult is Left, it applies the provided function to the error value,
|
||||
// which returns a new IOResult that replaces the original.
|
||||
//
|
||||
// This is useful for error recovery, fallback logic, or chaining alternative computations
|
||||
// in IO contexts. Since IOResult is specialized for error type, the error type remains error.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Recover from specific errors with fallback IO operations
|
||||
// recover := ioresult.OrElse(func(err error) ioresult.IOResult[int] {
|
||||
// if err.Error() == "not found" {
|
||||
// return ioresult.Right[int](0) // default value
|
||||
// }
|
||||
// return ioresult.Left[int](err) // propagate other errors
|
||||
// })
|
||||
// result := recover(ioresult.Left[int](errors.New("not found"))) // Right(0)
|
||||
// result := recover(ioresult.Right[int](42)) // Right(42) - unchanged
|
||||
//
|
||||
//go:inline
|
||||
func OrElse[A any](onLeft Kleisli[error, A]) Operator[A, A] {
|
||||
return ioeither.OrElse(onLeft)
|
||||
}
|
||||
|
||||
@@ -150,3 +150,51 @@ func TestApSecond(t *testing.T) {
|
||||
|
||||
assert.Equal(t, result.Of("b"), x())
|
||||
}
|
||||
|
||||
func TestOrElse(t *testing.T) {
|
||||
// Test basic recovery from Left
|
||||
recover := OrElse(func(e error) IOResult[int] {
|
||||
return Right(0)
|
||||
})
|
||||
|
||||
res := recover(Left[int](fmt.Errorf("error")))()
|
||||
assert.Equal(t, result.Of(0), res)
|
||||
|
||||
// Test Right value passes through unchanged
|
||||
res = recover(Right(42))()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
|
||||
// Test selective recovery - recover some errors, propagate others
|
||||
selectiveRecover := OrElse(func(err error) IOResult[int] {
|
||||
if err.Error() == "not found" {
|
||||
return Right(0) // default value for "not found"
|
||||
}
|
||||
return Left[int](err) // propagate other errors
|
||||
})
|
||||
notFoundResult := selectiveRecover(Left[int](fmt.Errorf("not found")))()
|
||||
assert.Equal(t, result.Of(0), notFoundResult)
|
||||
|
||||
permissionErr := fmt.Errorf("permission denied")
|
||||
permissionResult := selectiveRecover(Left[int](permissionErr))()
|
||||
assert.Equal(t, result.Left[int](permissionErr), permissionResult)
|
||||
|
||||
// Test chaining multiple OrElse operations
|
||||
firstRecover := OrElse(func(err error) IOResult[int] {
|
||||
if err.Error() == "error1" {
|
||||
return Right(1)
|
||||
}
|
||||
return Left[int](err)
|
||||
})
|
||||
secondRecover := OrElse(func(err error) IOResult[int] {
|
||||
if err.Error() == "error2" {
|
||||
return Right(2)
|
||||
}
|
||||
return Left[int](err)
|
||||
})
|
||||
|
||||
result1 := F.Pipe1(Left[int](fmt.Errorf("error1")), firstRecover)()
|
||||
assert.Equal(t, result.Of(1), result1)
|
||||
|
||||
result2 := F.Pipe1(Left[int](fmt.Errorf("error2")), F.Flow2(firstRecover, secondRecover))()
|
||||
assert.Equal(t, result.Of(2), result2)
|
||||
}
|
||||
|
||||
@@ -13,20 +13,41 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
IO[A any] = io.IO[A]
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
Result[A any] = result.Result[A]
|
||||
// IO represents a synchronous computation that cannot fail.
|
||||
IO[A any] = io.IO[A]
|
||||
|
||||
// Lazy represents a deferred computation that produces a value of type A.
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
|
||||
// Either represents a value of one of two possible types (a disjoint union).
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
|
||||
// Result represents a computation that may fail with an error.
|
||||
// It's an alias for Either[error, A].
|
||||
Result[A any] = result.Result[A]
|
||||
|
||||
// Endomorphism represents a function from a type to itself (A -> A).
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
// IOEither represents a synchronous computation that may fail
|
||||
// refer to [https://andywhite.xyz/posts/2021-01-27-rte-foundations/#ioeitherlte-agt] for more details
|
||||
IOResult[A any] = IO[Result[A]]
|
||||
Monoid[A any] = monoid.Monoid[IOResult[A]]
|
||||
// IOResult represents a synchronous computation that may fail with an error.
|
||||
// It combines IO (side effects) with Result (error handling).
|
||||
// Refer to [https://andywhite.xyz/posts/2021-01-27-rte-foundations/#ioeitherlte-agt] for more details.
|
||||
IOResult[A any] = IO[Result[A]]
|
||||
|
||||
// Monoid represents a monoid structure for IOResult values.
|
||||
Monoid[A any] = monoid.Monoid[IOResult[A]]
|
||||
|
||||
// Semigroup represents a semigroup structure for IOResult values.
|
||||
Semigroup[A any] = semigroup.Semigroup[IOResult[A]]
|
||||
|
||||
Kleisli[A, B any] = reader.Reader[A, IOResult[B]]
|
||||
// Kleisli represents a Kleisli arrow for the IOResult monad.
|
||||
// It's a function from A to IOResult[B], used for composing operations that may fail.
|
||||
Kleisli[A, B any] = reader.Reader[A, IOResult[B]]
|
||||
|
||||
// Operator represents a function that transforms one IOResult into another.
|
||||
// It takes an IOResult[A] and produces an IOResult[B].
|
||||
Operator[A, B any] = Kleisli[IOResult[A], B]
|
||||
|
||||
// Consumer represents a function that consumes a value of type A.
|
||||
Consumer[A any] = consumer.Consumer[A]
|
||||
)
|
||||
|
||||
@@ -94,8 +94,15 @@ type (
|
||||
// // optLens is a LensO[*Config, *int]
|
||||
LensO[S, A any] = Lens[S, Option[A]]
|
||||
|
||||
Kleisli[S, A, B any] = reader.Reader[A, LensO[S, B]]
|
||||
// Kleisli represents a Kleisli arrow for optional lenses.
|
||||
// It's a function from A to LensO[S, B], used for composing optional lens operations.
|
||||
Kleisli[S, A, B any] = reader.Reader[A, LensO[S, B]]
|
||||
|
||||
// Operator represents a function that transforms one optional lens into another.
|
||||
// It takes a LensO[S, A] and produces a LensO[S, B].
|
||||
Operator[S, A, B any] = Kleisli[S, LensO[S, A], B]
|
||||
|
||||
// Iso represents an isomorphism between types S and A.
|
||||
// An isomorphism is a bidirectional transformation that preserves structure.
|
||||
Iso[S, A any] = iso.Iso[S, A]
|
||||
)
|
||||
|
||||
@@ -115,7 +115,7 @@ func Id[S any]() Prism[S, S] {
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// positivePrism := FromPredicate(func(n int) bool { return n > 0 })
|
||||
// positivePrism := FromPredicate(N.MoreThan(0))
|
||||
// value := positivePrism.GetOption(42) // Some(42)
|
||||
// value = positivePrism.GetOption(-5) // None[int]
|
||||
func FromPredicate[S any](pred func(S) bool) Prism[S, S] {
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -283,7 +284,7 @@ func TestPrismModify(t *testing.T) {
|
||||
// TestPrismModifyWithTransform tests modifying through a prism with a transformation
|
||||
func TestPrismModifyWithTransform(t *testing.T) {
|
||||
// Create a prism for positive numbers
|
||||
positivePrism := FromPredicate(func(n int) bool { return n > 0 })
|
||||
positivePrism := FromPredicate(N.MoreThan(0))
|
||||
|
||||
// Modify positive number
|
||||
setter := Set[int](100)
|
||||
|
||||
@@ -6,7 +6,14 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
Traversal[S, A any] = T.Traversal[S, A, Result[S], Result[A]]
|
||||
Result[T any] = result.Result[T]
|
||||
// Traversal represents an optic that focuses on zero or more values of type A within a structure S.
|
||||
// It's specialized for Result types, allowing traversal over successful values.
|
||||
Traversal[S, A any] = T.Traversal[S, A, Result[S], Result[A]]
|
||||
|
||||
// Result represents a computation that may fail with an error.
|
||||
Result[T any] = result.Result[T]
|
||||
|
||||
// Operator represents a function that transforms one Traversal into another.
|
||||
// It takes a Traversal[S, A] and produces a Traversal[S, B].
|
||||
Operator[S, A, B any] = func(Traversal[S, A]) Traversal[S, B]
|
||||
)
|
||||
|
||||
@@ -73,7 +73,7 @@ func BenchmarkChain(b *testing.B) {
|
||||
|
||||
func BenchmarkFilter(b *testing.B) {
|
||||
opt := Some(42)
|
||||
filter := Filter(func(x int) bool { return x > 0 })
|
||||
filter := Filter(N.MoreThan(0))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -164,7 +164,7 @@ func BenchmarkDoBind(b *testing.B) {
|
||||
|
||||
// Benchmark conversions
|
||||
func BenchmarkFromPredicate(b *testing.B) {
|
||||
pred := FromPredicate(func(x int) bool { return x > 0 })
|
||||
pred := FromPredicate(N.MoreThan(0))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
||||
@@ -37,7 +37,7 @@ func fromPredicate[A any](a A, pred func(A) bool) Option[A] {
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// isPositive := FromPredicate(func(n int) bool { return n > 0 })
|
||||
// isPositive := FromPredicate(N.MoreThan(0))
|
||||
// result := isPositive(5) // Some(5)
|
||||
// result := isPositive(-1) // None
|
||||
func FromPredicate[A any](pred func(A) bool) Kleisli[A, A] {
|
||||
@@ -433,7 +433,7 @@ func Reduce[A, B any](f func(B, A) B, initial B) func(Option[A]) B {
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// isPositive := Filter(func(x int) bool { return x > 0 })
|
||||
// isPositive := Filter(N.MoreThan(0))
|
||||
// result := isPositive(Some(5)) // Some(5)
|
||||
// result := isPositive(Some(-1)) // None
|
||||
// result := isPositive(None[int]()) // None
|
||||
|
||||
@@ -180,7 +180,7 @@ func TestSequence2(t *testing.T) {
|
||||
|
||||
// Test Filter
|
||||
func TestFilter(t *testing.T) {
|
||||
isPositive := Filter(func(x int) bool { return x > 0 })
|
||||
isPositive := Filter(N.MoreThan(0))
|
||||
|
||||
assert.Equal(t, Some(5), isPositive(Some(5)))
|
||||
assert.Equal(t, None[int](), isPositive(Some(-1)))
|
||||
|
||||
@@ -47,7 +47,7 @@ import (
|
||||
// eqBool := eq.FromStrictEquals[bool]()
|
||||
//
|
||||
// ab := strconv.Itoa
|
||||
// bc := func(s string) bool { return len(s) > 0 }
|
||||
// bc := S.IsNonEmpty
|
||||
//
|
||||
// assert := AssertLaws(t, eqInt, eqString, eqBool, ab, bc)
|
||||
// assert(42) // verifies laws hold for value 42
|
||||
|
||||
@@ -7,6 +7,10 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
Seq[T any] = iter.Seq[T]
|
||||
// Seq represents an iterator sequence over values of type T.
|
||||
// It's an alias for Go's standard iter.Seq[T] type.
|
||||
Seq[T any] = iter.Seq[T]
|
||||
|
||||
// Endomorphism represents a function from a type to itself (T -> T).
|
||||
Endomorphism[T any] = endomorphism.Endomorphism[T]
|
||||
)
|
||||
|
||||
@@ -22,7 +22,7 @@ package predicate
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// isPositive := func(n int) bool { return n > 0 }
|
||||
// isPositive := N.MoreThan(0)
|
||||
// isNotPositive := Not(isPositive)
|
||||
// isNotPositive(5) // false
|
||||
// isNotPositive(-3) // true
|
||||
@@ -40,7 +40,7 @@ func Not[A any](predicate Predicate[A]) Predicate[A] {
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// isPositive := func(n int) bool { return n > 0 }
|
||||
// isPositive := N.MoreThan(0)
|
||||
// isEven := func(n int) bool { return n%2 == 0 }
|
||||
// isPositiveAndEven := F.Pipe1(isPositive, And(isEven))
|
||||
// isPositiveAndEven(4) // true
|
||||
@@ -62,7 +62,7 @@ func And[A any](second Predicate[A]) Operator[A, A] {
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// isPositive := func(n int) bool { return n > 0 }
|
||||
// isPositive := N.MoreThan(0)
|
||||
// isEven := func(n int) bool { return n%2 == 0 }
|
||||
// isPositiveOrEven := F.Pipe1(isPositive, Or(isEven))
|
||||
// isPositiveOrEven(4) // true
|
||||
|
||||
@@ -43,7 +43,7 @@ type (
|
||||
// Example:
|
||||
//
|
||||
// s := SemigroupAny[int]()
|
||||
// isPositive := func(n int) bool { return n > 0 }
|
||||
// isPositive := N.MoreThan(0)
|
||||
// isEven := func(n int) bool { return n%2 == 0 }
|
||||
// isPositiveOrEven := s.Concat(isPositive, isEven)
|
||||
// isPositiveOrEven(4) // true (even)
|
||||
@@ -71,7 +71,7 @@ func SemigroupAny[A any]() Semigroup[A] {
|
||||
// Example:
|
||||
//
|
||||
// s := SemigroupAll[int]()
|
||||
// isPositive := func(n int) bool { return n > 0 }
|
||||
// isPositive := N.MoreThan(0)
|
||||
// isEven := func(n int) bool { return n%2 == 0 }
|
||||
// isPositiveAndEven := s.Concat(isPositive, isEven)
|
||||
// isPositiveAndEven(4) // true (both)
|
||||
@@ -129,7 +129,7 @@ func MonoidAny[A any]() Monoid[A] {
|
||||
//
|
||||
// m := MonoidAll[int]()
|
||||
// predicates := []Predicate[int]{
|
||||
// func(n int) bool { return n > 0 },
|
||||
// N.MoreThan(0),
|
||||
// func(n int) bool { return n < 100 },
|
||||
// }
|
||||
// combined := A.Reduce(m.Empty(), m.Concat)(predicates)
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
// import P "github.com/IBM/fp-go/v2/predicate"
|
||||
//
|
||||
// // Create predicates
|
||||
// isPositive := func(n int) bool { return n > 0 }
|
||||
// isPositive := N.MoreThan(0)
|
||||
// isEven := func(n int) bool { return n%2 == 0 }
|
||||
//
|
||||
// // Combine predicates
|
||||
|
||||
@@ -98,10 +98,6 @@ func GetOrElse[E, L, A any](onLeft func(L) Reader[E, A]) func(ReaderEither[E, L,
|
||||
return eithert.GetOrElse(reader.MonadChain[E, Either[L, A], A], reader.Of[E, A], onLeft)
|
||||
}
|
||||
|
||||
func OrElse[E, L1, A, L2 any](onLeft func(L1) ReaderEither[E, L2, A]) func(ReaderEither[E, L1, A]) ReaderEither[E, L2, A] {
|
||||
return eithert.OrElse(reader.MonadChain[E, Either[L1, A], Either[L2, A]], reader.Of[E, Either[L2, A]], onLeft)
|
||||
}
|
||||
|
||||
func OrLeft[A, L1, E, L2 any](onLeft func(L1) Reader[E, L2]) func(ReaderEither[E, L1, A]) ReaderEither[E, L2, A] {
|
||||
return eithert.OrLeft(
|
||||
reader.MonadChain[E, Either[L1, A], Either[L2, A]],
|
||||
@@ -180,3 +176,228 @@ func MonadMapLeft[C, E1, E2, A any](fa ReaderEither[C, E1, A], f func(E1) E2) Re
|
||||
func MapLeft[C, E1, E2, A any](f func(E1) E2) func(ReaderEither[C, E1, A]) ReaderEither[C, E2, A] {
|
||||
return eithert.MapLeft(reader.Map[C, Either[E1, A], Either[E2, A]], f)
|
||||
}
|
||||
|
||||
// OrElse recovers from a Left (error) by providing an alternative computation with access to the reader context.
|
||||
// If the ReaderEither is Right, it returns the value unchanged.
|
||||
// If the ReaderEither is Left, it applies the provided function to the error value,
|
||||
// which returns a new ReaderEither that replaces the original.
|
||||
//
|
||||
// This is useful for error recovery, fallback logic, or chaining alternative computations
|
||||
// that need access to configuration or dependencies. The error type can be widened from E1 to E2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct{ fallbackValue int }
|
||||
//
|
||||
// // Recover using config-dependent fallback
|
||||
// recover := readereither.OrElse(func(err error) readereither.ReaderEither[Config, error, int] {
|
||||
// if err.Error() == "not found" {
|
||||
// return readereither.Asks[error](func(cfg Config) either.Either[error, int] {
|
||||
// return either.Right[error](cfg.fallbackValue)
|
||||
// })
|
||||
// }
|
||||
// return readereither.Left[Config, int](err)
|
||||
// })
|
||||
// result := recover(readereither.Left[Config, int](errors.New("not found")))(Config{fallbackValue: 42}) // Right(42)
|
||||
//
|
||||
//go:inline
|
||||
func OrElse[R, E1, E2, A any](onLeft Kleisli[R, E2, E1, A]) Kleisli[R, E2, ReaderEither[R, E1, A], A] {
|
||||
return Fold(onLeft, Of[R, E2, A])
|
||||
}
|
||||
|
||||
// MonadChainLeft chains a computation on the left (error) side of a ReaderEither.
|
||||
// If the input is a Left value, it applies the function f to transform the error and potentially
|
||||
// change the error type from EA to EB. If the input is a Right value, it passes through unchanged.
|
||||
//
|
||||
// This is useful for error recovery or error transformation scenarios where you want to handle
|
||||
// errors by performing another computation that may also fail, with access to configuration context.
|
||||
//
|
||||
// Note: This is functionally identical to the uncurried form of [OrElseW]. Use [ChainLeft] when
|
||||
// emphasizing the monadic chaining perspective, and [OrElseW] for error recovery semantics.
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: The input ReaderEither that may contain an error of type EA
|
||||
// - f: A Kleisli function that takes an error of type EA and returns a ReaderEither with error type EB
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderEither with the potentially transformed error type EB
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct{ fallbackValue int }
|
||||
// type ValidationError struct{ field string }
|
||||
// type SystemError struct{ code int }
|
||||
//
|
||||
// // Recover from validation errors using config
|
||||
// result := MonadChainLeft(
|
||||
// Left[Config, int](ValidationError{"username"}),
|
||||
// func(ve ValidationError) readereither.ReaderEither[Config, SystemError, int] {
|
||||
// if ve.field == "username" {
|
||||
// return Asks[SystemError](func(cfg Config) either.Either[SystemError, int] {
|
||||
// return either.Right[SystemError](cfg.fallbackValue)
|
||||
// })
|
||||
// }
|
||||
// return Left[Config, int](SystemError{400})
|
||||
// },
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainLeft[R, EA, EB, A any](fa ReaderEither[R, EA, A], f Kleisli[R, EB, EA, A]) ReaderEither[R, EB, A] {
|
||||
return func(r R) Either[EB, A] {
|
||||
return ET.Fold(
|
||||
func(ea EA) Either[EB, A] { return f(ea)(r) },
|
||||
ET.Right[EB, A],
|
||||
)(fa(r))
|
||||
}
|
||||
}
|
||||
|
||||
// ChainLeft is the curried version of [MonadChainLeft].
|
||||
// It returns a function that chains a computation on the left (error) side of a ReaderEither.
|
||||
//
|
||||
// This is particularly useful in functional composition pipelines where you want to handle
|
||||
// errors by performing another computation that may also fail, with access to configuration context.
|
||||
//
|
||||
// Note: This is functionally identical to [OrElseW]. They are different names for the same operation.
|
||||
// Use [ChainLeft] when emphasizing the monadic chaining perspective on the error channel,
|
||||
// and [OrElseW] when emphasizing error recovery/fallback semantics.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Kleisli function that takes an error of type EA and returns a ReaderEither with error type EB
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms a ReaderEither with error type EA to one with error type EB
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct{ retryLimit int }
|
||||
//
|
||||
// // Create a reusable error handler with config access
|
||||
// recoverFromError := ChainLeft(func(err string) readereither.ReaderEither[Config, int, string] {
|
||||
// if strings.Contains(err, "retryable") {
|
||||
// return Asks[int](func(cfg Config) either.Either[int, string] {
|
||||
// if cfg.retryLimit > 0 {
|
||||
// return either.Right[int]("recovered")
|
||||
// }
|
||||
// return either.Left[string](500)
|
||||
// })
|
||||
// }
|
||||
// return Left[Config, string](404)
|
||||
// })
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// Left[Config, string]("retryable error"),
|
||||
// recoverFromError,
|
||||
// )(Config{retryLimit: 3})
|
||||
//
|
||||
//go:inline
|
||||
func ChainLeft[R, EA, EB, A any](f Kleisli[R, EB, EA, A]) func(ReaderEither[R, EA, A]) ReaderEither[R, EB, A] {
|
||||
return func(fa ReaderEither[R, EA, A]) ReaderEither[R, EB, A] {
|
||||
return MonadChainLeft(fa, f)
|
||||
}
|
||||
}
|
||||
|
||||
// MonadChainFirstLeft chains a computation on the left (error) side but always returns the original error.
|
||||
// If the input is a Left value, it applies the function f to the error and executes the resulting computation,
|
||||
// but always returns the original Left error regardless of what f returns (Left or Right).
|
||||
// If the input is a Right value, it passes through unchanged without calling f.
|
||||
//
|
||||
// This is useful for side effects on errors (like logging or metrics) where you want to perform an action
|
||||
// when an error occurs but always propagate the original error, ensuring the error path is preserved.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The input ReaderEither that may contain an error of type EA
|
||||
// - f: A function that takes an error of type EA and returns a ReaderEither (typically for side effects)
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderEither with the original error preserved if input was Left, or the original Right value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct{ loggingEnabled bool }
|
||||
//
|
||||
// // Log errors but preserve the original error
|
||||
// result := MonadChainFirstLeft(
|
||||
// Left[Config, int]("database error"),
|
||||
// func(err string) readereither.ReaderEither[Config, string, int] {
|
||||
// return Asks[string](func(cfg Config) either.Either[string, int] {
|
||||
// if cfg.loggingEnabled {
|
||||
// log.Printf("Error: %s", err)
|
||||
// }
|
||||
// return either.Right[string](0)
|
||||
// })
|
||||
// },
|
||||
// )
|
||||
// // result will always be Left("database error")
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstLeft[A, R, EA, EB, B any](ma ReaderEither[R, EA, A], f Kleisli[R, EB, EA, B]) ReaderEither[R, EA, A] {
|
||||
return eithert.MonadChainFirstLeft(
|
||||
reader.MonadChain[R, Either[EA, A], Either[EA, A]],
|
||||
reader.MonadMap[R, Either[EB, B], Either[EA, A]],
|
||||
reader.Of[R, Either[EA, A]],
|
||||
ma,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadTapLeft[A, R, EA, EB, B any](ma ReaderEither[R, EA, A], f Kleisli[R, EB, EA, B]) ReaderEither[R, EA, A] {
|
||||
return MonadChainFirstLeft(ma, f)
|
||||
}
|
||||
|
||||
// ChainFirstLeft is the curried version of [MonadChainFirstLeft].
|
||||
// It returns a function that chains a computation on the left (error) side while always preserving the original error.
|
||||
//
|
||||
// This is particularly useful for adding error handling side effects (like logging, metrics, or notifications)
|
||||
// in a functional pipeline. The original error is always returned regardless of what f returns (Left or Right),
|
||||
// ensuring the error path is preserved.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that takes an error of type EA and returns a ReaderEither (typically for side effects)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that performs the side effect but always returns the original error if input was Left
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct{ metricsEnabled bool }
|
||||
//
|
||||
// // Create a reusable error logger
|
||||
// logError := ChainFirstLeft(func(err string) readereither.ReaderEither[Config, any, int] {
|
||||
// return Asks[any](func(cfg Config) either.Either[any, int] {
|
||||
// if cfg.metricsEnabled {
|
||||
// metrics.RecordError(err)
|
||||
// }
|
||||
// return either.Right[any](0)
|
||||
// })
|
||||
// })
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// Left[Config, int]("validation failed"),
|
||||
// logError, // records the error in metrics
|
||||
// )
|
||||
// // result is always Left("validation failed")
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstLeft[A, R, EA, EB, B any](f Kleisli[R, EB, EA, B]) Operator[R, EA, A, A] {
|
||||
return eithert.ChainFirstLeft(
|
||||
reader.Chain[R, Either[EA, A], Either[EA, A]],
|
||||
reader.Map[R, Either[EB, B], Either[EA, A]],
|
||||
reader.Of[R, Either[EA, A]],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TapLeft[A, R, EA, EB, B any](f Kleisli[R, EB, EA, B]) Operator[R, EA, A, A] {
|
||||
return ChainFirstLeft[A](f)
|
||||
}
|
||||
|
||||
// MonadFold applies one of two functions depending on the Either value.
|
||||
// If Left, applies onLeft function. If Right, applies onRight function.
|
||||
// Both functions return a Reader[E, B].
|
||||
//
|
||||
//go:inline
|
||||
func MonadFold[E, L, A, B any](ma ReaderEither[E, L, A], onLeft func(L) Reader[E, B], onRight func(A) Reader[E, B]) Reader[E, B] {
|
||||
return Fold(onLeft, onRight)(ma)
|
||||
}
|
||||
|
||||
@@ -57,3 +57,169 @@ func TestFlatten(t *testing.T) {
|
||||
|
||||
assert.Equal(t, ET.Of[string]("a"), g(defaultContext))
|
||||
}
|
||||
|
||||
func TestChainLeftFunc(t *testing.T) {
|
||||
type Config struct {
|
||||
errorCode int
|
||||
}
|
||||
|
||||
// Test with Right - should pass through unchanged
|
||||
t.Run("Right passes through", func(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Right[Config, string](42),
|
||||
ChainLeft(func(err string) ReaderEither[Config, int, int] {
|
||||
return Left[Config, int](999)
|
||||
}),
|
||||
)
|
||||
result := g(Config{errorCode: 500})
|
||||
assert.Equal(t, ET.Right[int](42), result)
|
||||
})
|
||||
|
||||
// Test with Left - error transformation with config
|
||||
t.Run("Left transforms error with config", func(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Left[Config, int]("error"),
|
||||
ChainLeft(func(err string) ReaderEither[Config, int, int] {
|
||||
return func(cfg Config) Either[int, int] {
|
||||
return ET.Left[int](cfg.errorCode)
|
||||
}
|
||||
}),
|
||||
)
|
||||
result := g(Config{errorCode: 500})
|
||||
assert.Equal(t, ET.Left[int](500), result)
|
||||
})
|
||||
|
||||
// Test with Left - successful recovery
|
||||
t.Run("Left recovers successfully", func(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Left[Config, int]("recoverable"),
|
||||
ChainLeft(func(err string) ReaderEither[Config, int, int] {
|
||||
if err == "recoverable" {
|
||||
return Right[Config, int](999)
|
||||
}
|
||||
return Left[Config, int](0)
|
||||
}),
|
||||
)
|
||||
result := g(Config{errorCode: 500})
|
||||
assert.Equal(t, ET.Right[int](999), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChainFirstLeftFunc(t *testing.T) {
|
||||
type Config struct {
|
||||
logEnabled bool
|
||||
}
|
||||
|
||||
logged := false
|
||||
|
||||
// Test with Right - should not call function
|
||||
t.Run("Right does not call function", func(t *testing.T) {
|
||||
logged = false
|
||||
g := F.Pipe1(
|
||||
Right[Config, string](42),
|
||||
ChainFirstLeft[int](func(err string) ReaderEither[Config, int, string] {
|
||||
logged = true
|
||||
return Right[Config, int]("logged")
|
||||
}),
|
||||
)
|
||||
result := g(Config{logEnabled: true})
|
||||
assert.Equal(t, ET.Right[string](42), result)
|
||||
assert.False(t, logged)
|
||||
})
|
||||
|
||||
// Test with Left - calls function but preserves original error
|
||||
t.Run("Left calls function but preserves error", func(t *testing.T) {
|
||||
logged = false
|
||||
g := F.Pipe1(
|
||||
Left[Config, int]("original error"),
|
||||
ChainFirstLeft[int](func(err string) ReaderEither[Config, int, string] {
|
||||
return func(cfg Config) Either[int, string] {
|
||||
if cfg.logEnabled {
|
||||
logged = true
|
||||
}
|
||||
return ET.Right[int]("side effect done")
|
||||
}
|
||||
}),
|
||||
)
|
||||
result := g(Config{logEnabled: true})
|
||||
assert.Equal(t, ET.Left[int]("original error"), result)
|
||||
assert.True(t, logged)
|
||||
})
|
||||
|
||||
// Test with Left - preserves original error even if side effect fails
|
||||
t.Run("Left preserves error even if side effect fails", func(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Left[Config, int]("original error"),
|
||||
ChainFirstLeft[int](func(err string) ReaderEither[Config, int, string] {
|
||||
return Left[Config, string](999) // Side effect fails
|
||||
}),
|
||||
)
|
||||
result := g(Config{logEnabled: true})
|
||||
assert.Equal(t, ET.Left[int]("original error"), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTapLeftFunc(t *testing.T) {
|
||||
// TapLeft is an alias for ChainFirstLeft, so just a basic sanity test
|
||||
type Config struct{}
|
||||
|
||||
sideEffectRan := false
|
||||
|
||||
g := F.Pipe1(
|
||||
Left[Config, int]("error"),
|
||||
TapLeft[int](func(err string) ReaderEither[Config, string, int] {
|
||||
sideEffectRan = true
|
||||
return Right[Config, string](0)
|
||||
}),
|
||||
)
|
||||
|
||||
result := g(Config{})
|
||||
assert.Equal(t, ET.Left[int]("error"), result)
|
||||
assert.True(t, sideEffectRan)
|
||||
}
|
||||
|
||||
func TestOrElse(t *testing.T) {
|
||||
type Config struct {
|
||||
fallbackValue int
|
||||
}
|
||||
|
||||
// Test OrElse with Right - should pass through unchanged
|
||||
rightValue := Of[Config, string](42)
|
||||
recover := OrElse(func(err string) ReaderEither[Config, string, int] {
|
||||
return Left[Config, int]("should not be called")
|
||||
})
|
||||
result := recover(rightValue)(Config{fallbackValue: 0})
|
||||
assert.Equal(t, ET.Right[string](42), result)
|
||||
|
||||
// Test OrElse with Left - should recover with fallback
|
||||
leftValue := Left[Config, int]("not found")
|
||||
recoverWithFallback := OrElse(func(err string) ReaderEither[Config, string, int] {
|
||||
if err == "not found" {
|
||||
return func(cfg Config) ET.Either[string, int] {
|
||||
return ET.Right[string](cfg.fallbackValue)
|
||||
}
|
||||
}
|
||||
return Left[Config, int](err)
|
||||
})
|
||||
result = recoverWithFallback(leftValue)(Config{fallbackValue: 99})
|
||||
assert.Equal(t, ET.Right[string](99), result)
|
||||
|
||||
// Test OrElse with Left - should propagate other errors
|
||||
leftValue = Left[Config, int]("fatal error")
|
||||
result = recoverWithFallback(leftValue)(Config{fallbackValue: 99})
|
||||
assert.Equal(t, ET.Left[int]("fatal error"), result)
|
||||
|
||||
// Test error type widening
|
||||
type ValidationError struct{ field string }
|
||||
type AppError struct{ code int }
|
||||
|
||||
validationErr := Left[Config, int](ValidationError{field: "username"})
|
||||
wideningRecover := OrElse(func(ve ValidationError) ReaderEither[Config, AppError, int] {
|
||||
if ve.field == "username" {
|
||||
return Right[Config, AppError](100)
|
||||
}
|
||||
return Left[Config, int](AppError{code: 400})
|
||||
})
|
||||
appResult := wideningRecover(validationErr)(Config{})
|
||||
assert.Equal(t, ET.Right[AppError](100), appResult)
|
||||
}
|
||||
|
||||
@@ -22,12 +22,26 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
Option[A any] = option.Option[A]
|
||||
// Option represents an optional value that may or may not be present.
|
||||
Option[A any] = option.Option[A]
|
||||
|
||||
// Either represents a value of one of two possible types (a disjoint union).
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
|
||||
// Reader represents a computation that depends on an environment R and produces a value A.
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
|
||||
// ReaderEither represents a computation that depends on an environment R and can fail
|
||||
// with an error E or succeed with a value A.
|
||||
// It combines Reader (dependency injection) with Either (error handling).
|
||||
ReaderEither[R, E, A any] = Reader[R, Either[E, A]]
|
||||
|
||||
Kleisli[R, E, A, B any] = Reader[A, ReaderEither[R, E, B]]
|
||||
// Kleisli represents a Kleisli arrow for the ReaderEither monad.
|
||||
// It's a function from A to ReaderEither[R, E, B], used for composing operations that
|
||||
// depend on an environment and may fail.
|
||||
Kleisli[R, E, A, B any] = Reader[A, ReaderEither[R, E, B]]
|
||||
|
||||
// Operator represents a function that transforms one ReaderEither into another.
|
||||
// It takes a ReaderEither[R, E, A] and produces a ReaderEither[R, E, B].
|
||||
Operator[R, E, A, B any] = Kleisli[R, E, ReaderEither[R, E, A], B]
|
||||
)
|
||||
|
||||
@@ -6,6 +6,27 @@ import (
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
)
|
||||
|
||||
// MonadReduceArray reduces an array of ReaderIOEither values into a single ReaderIOEither
|
||||
// by applying a reduction function to accumulate the success values.
|
||||
//
|
||||
// If any ReaderIOEither in the array fails, the entire operation fails with that error.
|
||||
// The reduction is performed sequentially from left to right.
|
||||
//
|
||||
// Type parameters:
|
||||
// - R: The context type
|
||||
// - E: The error type
|
||||
// - A: The element type in the array
|
||||
// - B: The accumulated result type
|
||||
//
|
||||
// Parameters:
|
||||
// - as: Array of ReaderIOEither values to reduce
|
||||
// - reduce: Function that combines the accumulator with each element
|
||||
// - initial: Initial value for the accumulator
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// A ReaderIOEither containing the final accumulated value
|
||||
//
|
||||
//go:inline
|
||||
func MonadReduceArray[R, E, A, B any](as []ReaderIOEither[R, E, A], reduce func(B, A) B, initial B) ReaderIOEither[R, E, B] {
|
||||
return RA.MonadTraverseReduce(
|
||||
@@ -21,6 +42,9 @@ func MonadReduceArray[R, E, A, B any](as []ReaderIOEither[R, E, A], reduce func(
|
||||
)
|
||||
}
|
||||
|
||||
// ReduceArray returns a function that reduces an array of ReaderIOEither values.
|
||||
// This is the curried version of MonadReduceArray.
|
||||
//
|
||||
//go:inline
|
||||
func ReduceArray[R, E, A, B any](reduce func(B, A) B, initial B) Kleisli[R, E, []ReaderIOEither[R, E, A], B] {
|
||||
return RA.TraverseReduce[[]ReaderIOEither[R, E, A]](
|
||||
@@ -34,16 +58,45 @@ func ReduceArray[R, E, A, B any](reduce func(B, A) B, initial B) Kleisli[R, E, [
|
||||
)
|
||||
}
|
||||
|
||||
// MonadReduceArrayM reduces an array of ReaderIOEither values using a monoid.
|
||||
// The monoid provides both the combination operation and the initial (empty) value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadReduceArrayM[R, E, A any](as []ReaderIOEither[R, E, A], m monoid.Monoid[A]) ReaderIOEither[R, E, A] {
|
||||
return MonadReduceArray(as, m.Concat, m.Empty())
|
||||
}
|
||||
|
||||
// ReduceArrayM returns a function that reduces an array using a monoid.
|
||||
// This is the curried version of MonadReduceArrayM.
|
||||
//
|
||||
//go:inline
|
||||
func ReduceArrayM[R, E, A any](m monoid.Monoid[A]) Kleisli[R, E, []ReaderIOEither[R, E, A], A] {
|
||||
return ReduceArray[R, E](m.Concat, m.Empty())
|
||||
}
|
||||
|
||||
// MonadTraverseReduceArray transforms each element of an array using a function that returns
|
||||
// a ReaderIOEither, then reduces the results into a single accumulated value.
|
||||
//
|
||||
// This combines traverse and reduce operations: it maps over the array with an effectful
|
||||
// function and simultaneously accumulates the results.
|
||||
//
|
||||
// Type parameters:
|
||||
// - R: The context type
|
||||
// - E: The error type
|
||||
// - A: The input element type
|
||||
// - B: The transformed element type
|
||||
// - C: The accumulated result type
|
||||
//
|
||||
// Parameters:
|
||||
// - as: Array of input values
|
||||
// - trfrm: Function that transforms each element into a ReaderIOEither
|
||||
// - reduce: Function that combines the accumulator with each transformed element
|
||||
// - initial: Initial value for the accumulator
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// A ReaderIOEither containing the final accumulated value
|
||||
//
|
||||
//go:inline
|
||||
func MonadTraverseReduceArray[R, E, A, B, C any](as []A, trfrm Kleisli[R, E, A, B], reduce func(C, B) C, initial C) ReaderIOEither[R, E, C] {
|
||||
return RA.MonadTraverseReduce(
|
||||
@@ -59,6 +112,9 @@ func MonadTraverseReduceArray[R, E, A, B, C any](as []A, trfrm Kleisli[R, E, A,
|
||||
)
|
||||
}
|
||||
|
||||
// TraverseReduceArray returns a function that traverses and reduces an array.
|
||||
// This is the curried version of MonadTraverseReduceArray.
|
||||
//
|
||||
//go:inline
|
||||
func TraverseReduceArray[R, E, A, B, C any](trfrm Kleisli[R, E, A, B], reduce func(C, B) C, initial C) Kleisli[R, E, []A, C] {
|
||||
return RA.TraverseReduce[[]A](
|
||||
@@ -72,11 +128,17 @@ func TraverseReduceArray[R, E, A, B, C any](trfrm Kleisli[R, E, A, B], reduce fu
|
||||
)
|
||||
}
|
||||
|
||||
// MonadTraverseReduceArrayM transforms and reduces an array using a monoid.
|
||||
// The monoid provides both the combination operation and the initial value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadTraverseReduceArrayM[R, E, A, B any](as []A, trfrm Kleisli[R, E, A, B], m monoid.Monoid[B]) ReaderIOEither[R, E, B] {
|
||||
return MonadTraverseReduceArray(as, trfrm, m.Concat, m.Empty())
|
||||
}
|
||||
|
||||
// TraverseReduceArrayM returns a function that traverses and reduces using a monoid.
|
||||
// This is the curried version of MonadTraverseReduceArrayM.
|
||||
//
|
||||
//go:inline
|
||||
func TraverseReduceArrayM[R, E, A, B any](trfrm Kleisli[R, E, A, B], m monoid.Monoid[B]) Kleisli[R, E, []A, B] {
|
||||
return TraverseReduceArray(trfrm, m.Concat, m.Empty())
|
||||
|
||||
@@ -2,11 +2,47 @@ package readerioeither
|
||||
|
||||
import "github.com/IBM/fp-go/v2/io"
|
||||
|
||||
// ChainConsumer chains a consumer (side-effect function) into a ReaderIOEither computation,
|
||||
// replacing the success value with an empty struct.
|
||||
//
|
||||
// This is useful for performing side effects (like logging or printing) where you don't
|
||||
// need to preserve the original value.
|
||||
//
|
||||
// Type parameters:
|
||||
// - R: The context type
|
||||
// - E: The error type
|
||||
// - A: The value type to consume
|
||||
//
|
||||
// Parameters:
|
||||
// - c: A consumer function that performs a side effect
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// An Operator that executes the consumer and returns struct{}
|
||||
//
|
||||
//go:inline
|
||||
func ChainConsumer[R, E, A any](c Consumer[A]) Operator[R, E, A, struct{}] {
|
||||
return ChainIOK[R, E](io.FromConsumerK(c))
|
||||
}
|
||||
|
||||
// ChainFirstConsumer chains a consumer into a ReaderIOEither computation while preserving
|
||||
// the original value.
|
||||
//
|
||||
// This is useful for performing side effects (like logging or printing) where you want
|
||||
// to keep the original value for further processing.
|
||||
//
|
||||
// Type parameters:
|
||||
// - R: The context type
|
||||
// - E: The error type
|
||||
// - A: The value type to consume
|
||||
//
|
||||
// Parameters:
|
||||
// - c: A consumer function that performs a side effect
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// An Operator that executes the consumer and returns the original value
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstConsumer[R, E, A any](c Consumer[A]) Operator[R, E, A, A] {
|
||||
return ChainFirstIOK[R, E](io.FromConsumerK(c))
|
||||
|
||||
61
v2/readerioeither/filter.go
Normal file
61
v2/readerioeither/filter.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioeither
|
||||
|
||||
import "github.com/IBM/fp-go/v2/either"
|
||||
|
||||
// FilterOrElse filters a ReaderIOEither value based on a predicate.
|
||||
// If the predicate returns true for the Right value, it passes through unchanged.
|
||||
// If the predicate returns false, it transforms the Right value into a Left using onFalse.
|
||||
// Left values are passed through unchanged.
|
||||
//
|
||||
// This is useful for adding validation or constraints to successful computations that
|
||||
// depend on a context, converting values that don't meet certain criteria into errors.
|
||||
//
|
||||
// Parameters:
|
||||
// - pred: A predicate function that tests the Right value
|
||||
// - onFalse: A function that converts the failing value into an error of type E
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that filters ReaderIOEither values based on the predicate
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// MaxValue int
|
||||
// }
|
||||
//
|
||||
// // Validate that a number doesn't exceed the configured maximum
|
||||
// validateMax := func(cfg Config) readerioeither.ReaderIOEither[Config, string, int] {
|
||||
// isValid := func(n int) bool { return n <= cfg.MaxValue }
|
||||
// onInvalid := func(n int) string {
|
||||
// return fmt.Sprintf("%d exceeds max %d", n, cfg.MaxValue)
|
||||
// }
|
||||
//
|
||||
// filter := readerioeither.FilterOrElse(isValid, onInvalid)
|
||||
// return filter(readerioeither.Right[Config, string](42))
|
||||
// }
|
||||
//
|
||||
// cfg := Config{MaxValue: 100}
|
||||
// result := validateMax(cfg)(cfg)() // Right(42)
|
||||
//
|
||||
// cfg2 := Config{MaxValue: 10}
|
||||
// result2 := validateMax(cfg2)(cfg2)() // Left("42 exceeds max 10")
|
||||
//
|
||||
//go:inline
|
||||
func FilterOrElse[R, E, A any](pred Predicate[A], onFalse func(A) E) Operator[R, E, A, A] {
|
||||
return ChainEitherK[R](either.FromPredicate(pred, onFalse))
|
||||
}
|
||||
295
v2/readerioeither/filter_test.go
Normal file
295
v2/readerioeither/filter_test.go
Normal file
@@ -0,0 +1,295 @@
|
||||
// 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 readerioeither
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
MaxValue int
|
||||
MinValue int
|
||||
}
|
||||
|
||||
func TestFilterOrElse_PredicateTrue(t *testing.T) {
|
||||
// Test that when predicate returns true, Right value passes through
|
||||
isPositive := N.MoreThan(0)
|
||||
onFalse := S.Format[int]("%d is not positive")
|
||||
|
||||
filter := FilterOrElse[context.Context](isPositive, onFalse)
|
||||
result := filter(Right[context.Context, string](42))(context.Background())()
|
||||
|
||||
assert.Equal(t, E.Right[string](42), result)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_PredicateFalse(t *testing.T) {
|
||||
// Test that when predicate returns false, Right value becomes Left
|
||||
isPositive := N.MoreThan(0)
|
||||
onFalse := S.Format[int]("%d is not positive")
|
||||
|
||||
filter := FilterOrElse[context.Context](isPositive, onFalse)
|
||||
result := filter(Right[context.Context, string](-5))(context.Background())()
|
||||
|
||||
assert.Equal(t, E.Left[int]("-5 is not positive"), result)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_LeftPassesThrough(t *testing.T) {
|
||||
// Test that Left values pass through unchanged
|
||||
isPositive := N.MoreThan(0)
|
||||
onFalse := S.Format[int]("%d is not positive")
|
||||
|
||||
filter := FilterOrElse[context.Context](isPositive, onFalse)
|
||||
result := filter(Left[context.Context, int]("original error"))(context.Background())()
|
||||
|
||||
assert.Equal(t, E.Left[int]("original error"), result)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_WithContext(t *testing.T) {
|
||||
// Test filtering with context-dependent validation
|
||||
cfg := Config{MaxValue: 100, MinValue: 0}
|
||||
|
||||
isInRange := func(n int) bool { return n >= cfg.MinValue && n <= cfg.MaxValue }
|
||||
onOutOfRange := func(n int) string {
|
||||
return fmt.Sprintf("%d is out of range [%d, %d]", n, cfg.MinValue, cfg.MaxValue)
|
||||
}
|
||||
|
||||
filter := FilterOrElse[Config](isInRange, onOutOfRange)
|
||||
|
||||
// Within range
|
||||
result1 := filter(Right[Config, string](50))(cfg)()
|
||||
assert.Equal(t, E.Right[string](50), result1)
|
||||
|
||||
// Below range
|
||||
result2 := filter(Right[Config, string](-10))(cfg)()
|
||||
assert.Equal(t, E.Left[int]("-10 is out of range [0, 100]"), result2)
|
||||
|
||||
// Above range
|
||||
result3 := filter(Right[Config, string](150))(cfg)()
|
||||
assert.Equal(t, E.Left[int]("150 is out of range [0, 100]"), result3)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_ZeroValue(t *testing.T) {
|
||||
// Test filtering with zero value
|
||||
isNonZero := func(n int) bool { return n != 0 }
|
||||
onZero := func(n int) string { return "value is zero" }
|
||||
|
||||
filter := FilterOrElse[context.Context](isNonZero, onZero)
|
||||
result := filter(Right[context.Context, string](0))(context.Background())()
|
||||
|
||||
assert.Equal(t, E.Left[int]("value is zero"), result)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_StringValidation(t *testing.T) {
|
||||
// Test with string validation
|
||||
isNonEmpty := S.IsNonEmpty
|
||||
onEmpty := func(s string) error { return fmt.Errorf("string is empty") }
|
||||
|
||||
filter := FilterOrElse[context.Context](isNonEmpty, onEmpty)
|
||||
|
||||
// Non-empty string passes
|
||||
result1 := filter(Right[context.Context, error]("hello"))(context.Background())()
|
||||
assert.Equal(t, E.Right[error]("hello"), result1)
|
||||
|
||||
// Empty string becomes error
|
||||
result2 := filter(Right[context.Context, error](""))(context.Background())()
|
||||
assert.True(t, E.IsLeft(result2))
|
||||
assert.Equal(t, "string is empty", E.ToError(result2).Error())
|
||||
}
|
||||
|
||||
func TestFilterOrElse_ComplexPredicate(t *testing.T) {
|
||||
// Test with more complex predicate
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
isAdult := func(u User) bool { return u.Age >= 18 }
|
||||
onMinor := func(u User) string {
|
||||
return fmt.Sprintf("%s is only %d years old", u.Name, u.Age)
|
||||
}
|
||||
|
||||
filter := FilterOrElse[context.Context](isAdult, onMinor)
|
||||
|
||||
// Adult user passes
|
||||
adult := User{Name: "Alice", Age: 25}
|
||||
result1 := filter(Right[context.Context, string](adult))(context.Background())()
|
||||
assert.Equal(t, E.Right[string](adult), result1)
|
||||
|
||||
// Minor becomes error
|
||||
minor := User{Name: "Bob", Age: 16}
|
||||
result2 := filter(Right[context.Context, string](minor))(context.Background())()
|
||||
assert.Equal(t, E.Left[User]("Bob is only 16 years old"), result2)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_ChainedFilters(t *testing.T) {
|
||||
// Test chaining multiple filters
|
||||
isPositive := N.MoreThan(0)
|
||||
onNegative := func(n int) string { return "not positive" }
|
||||
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
onOdd := func(n int) string { return "not even" }
|
||||
|
||||
filter1 := FilterOrElse[context.Context](isPositive, onNegative)
|
||||
filter2 := FilterOrElse[context.Context](isEven, onOdd)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Chain filters - apply filter1 first, then filter2
|
||||
result := filter2(filter1(Right[context.Context, string](4)))(ctx)()
|
||||
assert.Equal(t, E.Right[string](4), result)
|
||||
|
||||
// Fails first filter
|
||||
result2 := filter2(filter1(Right[context.Context, string](-2)))(ctx)()
|
||||
assert.Equal(t, E.Left[int]("not positive"), result2)
|
||||
|
||||
// Passes first but fails second
|
||||
result3 := filter2(filter1(Right[context.Context, string](3)))(ctx)()
|
||||
assert.Equal(t, E.Left[int]("not even"), result3)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_WithMap(t *testing.T) {
|
||||
// Test FilterOrElse combined with Map
|
||||
isPositive := N.MoreThan(0)
|
||||
onNegative := func(n int) string { return "negative number" }
|
||||
|
||||
filter := FilterOrElse[context.Context](isPositive, onNegative)
|
||||
double := Map[context.Context, string](func(n int) int { return n * 2 })
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Compose: filter then double
|
||||
result1 := double(filter(Right[context.Context, string](5)))(ctx)()
|
||||
assert.Equal(t, E.Right[string](10), result1)
|
||||
|
||||
// Negative value filtered out
|
||||
result2 := double(filter(Right[context.Context, string](-3)))(ctx)()
|
||||
assert.Equal(t, E.Left[int]("negative number"), result2)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_BoundaryConditions(t *testing.T) {
|
||||
// Test boundary conditions
|
||||
isInRange := func(n int) bool { return n >= 0 && n <= 100 }
|
||||
onOutOfRange := func(n int) string {
|
||||
return fmt.Sprintf("%d is out of range [0, 100]", n)
|
||||
}
|
||||
|
||||
filter := FilterOrElse[context.Context](isInRange, onOutOfRange)
|
||||
ctx := context.Background()
|
||||
|
||||
// Lower boundary
|
||||
result1 := filter(Right[context.Context, string](0))(ctx)()
|
||||
assert.Equal(t, E.Right[string](0), result1)
|
||||
|
||||
// Upper boundary
|
||||
result2 := filter(Right[context.Context, string](100))(ctx)()
|
||||
assert.Equal(t, E.Right[string](100), result2)
|
||||
|
||||
// Below lower boundary
|
||||
result3 := filter(Right[context.Context, string](-1))(ctx)()
|
||||
assert.Equal(t, E.Left[int]("-1 is out of range [0, 100]"), result3)
|
||||
|
||||
// Above upper boundary
|
||||
result4 := filter(Right[context.Context, string](101))(ctx)()
|
||||
assert.Equal(t, E.Left[int]("101 is out of range [0, 100]"), result4)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_AlwaysTrue(t *testing.T) {
|
||||
// Test with predicate that always returns true
|
||||
alwaysTrue := func(n int) bool { return true }
|
||||
onFalse := func(n int) string { return "never happens" }
|
||||
|
||||
filter := FilterOrElse[context.Context](alwaysTrue, onFalse)
|
||||
ctx := context.Background()
|
||||
|
||||
result1 := filter(Right[context.Context, string](42))(ctx)()
|
||||
assert.Equal(t, E.Right[string](42), result1)
|
||||
|
||||
result2 := filter(Right[context.Context, string](-42))(ctx)()
|
||||
assert.Equal(t, E.Right[string](-42), result2)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_AlwaysFalse(t *testing.T) {
|
||||
// Test with predicate that always returns false
|
||||
alwaysFalse := func(n int) bool { return false }
|
||||
onFalse := S.Format[int]("rejected: %d")
|
||||
|
||||
filter := FilterOrElse[context.Context](alwaysFalse, onFalse)
|
||||
ctx := context.Background()
|
||||
|
||||
result := filter(Right[context.Context, string](42))(ctx)()
|
||||
assert.Equal(t, E.Left[int]("rejected: 42"), result)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_NilPointerValidation(t *testing.T) {
|
||||
// Test filtering nil pointers
|
||||
isNonNil := func(p *int) bool { return p != nil }
|
||||
onNil := func(p *int) string { return "pointer is nil" }
|
||||
|
||||
filter := FilterOrElse[context.Context](isNonNil, onNil)
|
||||
ctx := context.Background()
|
||||
|
||||
// Non-nil pointer passes
|
||||
value := 42
|
||||
result1 := filter(Right[context.Context, string](&value))(ctx)()
|
||||
assert.True(t, E.IsRight(result1))
|
||||
|
||||
// Nil pointer becomes error
|
||||
result2 := filter(Right[context.Context, string]((*int)(nil)))(ctx)()
|
||||
assert.Equal(t, E.Left[*int]("pointer is nil"), result2)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_ContextPropagation(t *testing.T) {
|
||||
// Test that context is properly propagated
|
||||
type ctxKey string
|
||||
const key ctxKey = "test-key"
|
||||
|
||||
ctx := context.WithValue(context.Background(), key, "test-value")
|
||||
|
||||
isPositive := N.MoreThan(0)
|
||||
onNegative := func(n int) string { return "negative" }
|
||||
|
||||
filter := FilterOrElse[context.Context](isPositive, onNegative)
|
||||
|
||||
// The context should be available when the computation runs
|
||||
result := filter(Right[context.Context, string](42))(ctx)()
|
||||
assert.Equal(t, E.Right[string](42), result)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_DifferentContextTypes(t *testing.T) {
|
||||
// Test with different context types
|
||||
type AppConfig struct {
|
||||
Name string
|
||||
Version string
|
||||
}
|
||||
|
||||
cfg := AppConfig{Name: "TestApp", Version: "1.0.0"}
|
||||
|
||||
isValidVersion := func(v string) bool { return len(v) > 0 }
|
||||
onInvalid := func(v string) error { return fmt.Errorf("invalid version") }
|
||||
|
||||
filter := FilterOrElse[AppConfig](isValidVersion, onInvalid)
|
||||
|
||||
result := filter(Right[AppConfig, error]("1.0.0"))(cfg)()
|
||||
assert.Equal(t, E.Right[error]("1.0.0"), result)
|
||||
}
|
||||
@@ -24,16 +24,16 @@ type (
|
||||
Monoid[R, E, A any] = monoid.Monoid[ReaderIOEither[R, E, A]]
|
||||
)
|
||||
|
||||
// ApplicativeMonoid returns a [Monoid] that concatenates [ReaderIOResult] instances via their applicative.
|
||||
// ApplicativeMonoid returns a [Monoid] that concatenates [ReaderIOEither] instances via their applicative.
|
||||
// This uses the default applicative behavior (parallel or sequential based on useParallel flag).
|
||||
//
|
||||
// The monoid combines two ReaderIOResult values by applying the underlying monoid's combine operation
|
||||
// The monoid combines two ReaderIOEither values by applying the underlying monoid's combine operation
|
||||
// to their success values using applicative application.
|
||||
//
|
||||
// Parameters:
|
||||
// - m: The underlying monoid for type A
|
||||
//
|
||||
// Returns a Monoid for ReaderIOResult[A].
|
||||
// Returns a Monoid for ReaderIOEither[R, E, A].
|
||||
func ApplicativeMonoid[R, E, A any](m monoid.Monoid[A]) Monoid[R, E, A] {
|
||||
return monoid.ApplicativeMonoid(
|
||||
Of[R, E, A],
|
||||
@@ -43,13 +43,13 @@ func ApplicativeMonoid[R, E, A any](m monoid.Monoid[A]) Monoid[R, E, A] {
|
||||
)
|
||||
}
|
||||
|
||||
// ApplicativeMonoidSeq returns a [Monoid] that concatenates [ReaderIOResult] instances via their applicative.
|
||||
// ApplicativeMonoidSeq returns a [Monoid] that concatenates [ReaderIOEither] instances via their applicative.
|
||||
// This explicitly uses sequential execution for combining values.
|
||||
//
|
||||
// Parameters:
|
||||
// - m: The underlying monoid for type A
|
||||
//
|
||||
// Returns a Monoid for ReaderIOResult[A] with sequential execution.
|
||||
// Returns a Monoid for ReaderIOEither[R, E, A] with sequential execution.
|
||||
func ApplicativeMonoidSeq[R, E, A any](m monoid.Monoid[A]) Monoid[R, E, A] {
|
||||
return monoid.ApplicativeMonoid(
|
||||
Of[R, E, A],
|
||||
@@ -59,13 +59,13 @@ func ApplicativeMonoidSeq[R, E, A any](m monoid.Monoid[A]) Monoid[R, E, A] {
|
||||
)
|
||||
}
|
||||
|
||||
// ApplicativeMonoidPar returns a [Monoid] that concatenates [ReaderIOResult] instances via their applicative.
|
||||
// ApplicativeMonoidPar returns a [Monoid] that concatenates [ReaderIOEither] instances via their applicative.
|
||||
// This explicitly uses parallel execution for combining values.
|
||||
//
|
||||
// Parameters:
|
||||
// - m: The underlying monoid for type A
|
||||
//
|
||||
// Returns a Monoid for ReaderIOResult[A] with parallel execution.
|
||||
// Returns a Monoid for ReaderIOEither[R, E, A] with parallel execution.
|
||||
func ApplicativeMonoidPar[R, E, A any](m monoid.Monoid[A]) Monoid[R, E, A] {
|
||||
return monoid.ApplicativeMonoid(
|
||||
Of[R, E, A],
|
||||
@@ -75,14 +75,14 @@ func ApplicativeMonoidPar[R, E, A any](m monoid.Monoid[A]) Monoid[R, E, A] {
|
||||
)
|
||||
}
|
||||
|
||||
// AlternativeMonoid is the alternative [Monoid] for [ReaderIOResult].
|
||||
// This combines ReaderIOResult values using the alternative semantics,
|
||||
// AlternativeMonoid is the alternative [Monoid] for [ReaderIOEither].
|
||||
// This combines ReaderIOEither values using the alternative semantics,
|
||||
// where the second value is only evaluated if the first fails.
|
||||
//
|
||||
// Parameters:
|
||||
// - m: The underlying monoid for type A
|
||||
//
|
||||
// Returns a Monoid for ReaderIOResult[A] with alternative semantics.
|
||||
// Returns a Monoid for ReaderIOEither[R, E, A] with alternative semantics.
|
||||
func AlternativeMonoid[R, E, A any](m monoid.Monoid[A]) Monoid[R, E, A] {
|
||||
return monoid.AlternativeMonoid(
|
||||
Of[R, E, A],
|
||||
@@ -93,14 +93,14 @@ func AlternativeMonoid[R, E, A any](m monoid.Monoid[A]) Monoid[R, E, A] {
|
||||
)
|
||||
}
|
||||
|
||||
// AltMonoid is the alternative [Monoid] for a [ReaderIOResult].
|
||||
// AltMonoid is the alternative [Monoid] for a [ReaderIOEither].
|
||||
// This creates a monoid where the empty value is provided lazily,
|
||||
// and combination uses the Alt operation (try first, fallback to second on failure).
|
||||
//
|
||||
// Parameters:
|
||||
// - zero: Lazy computation that provides the empty/identity value
|
||||
//
|
||||
// Returns a Monoid for ReaderIOResult[A] with Alt-based combination.
|
||||
// Returns a Monoid for ReaderIOEither[R, E, A] with Alt-based combination.
|
||||
func AltMonoid[R, E, A any](zero lazy.Lazy[ReaderIOEither[R, E, A]]) Monoid[R, E, A] {
|
||||
return monoid.AltMonoid(
|
||||
zero,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user