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

Compare commits

...

7 Commits

Author SHA1 Message Date
Dr. Carsten Leue
2329edea36 fix: signature of Close
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-31 15:53:28 +01:00
Dr. Carsten Leue
5b910a39af fix: add tests for CopyFile
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-31 15:49:30 +01:00
Dr. Carsten Leue
5ba6bd9583 fix: add sample
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-31 15:41:28 +01:00
Dr. Carsten Leue
d6df9ab738 fix: tests and doc
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-30 17:47:16 +01:00
Dr. Carsten Leue
f139aab2b8 fix: FilterOrElse
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-30 14:39:04 +01:00
Dr. Carsten Leue
638c6357da fix: improve tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-30 11:15:23 +01:00
Carsten Leue
451cbc8bf6 fix: OrElse
Signed-off-by: Carsten Leue <carsten.leue@de.ibm.com>
2025-12-26 15:05:19 +01:00
134 changed files with 6540 additions and 314 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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