1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-12-30 00:09:39 +02:00

Compare commits

...

1 Commits

Author SHA1 Message Date
Carsten Leue
451cbc8bf6 fix: OrElse
Signed-off-by: Carsten Leue <carsten.leue@de.ibm.com>
2025-12-26 15:05:19 +01:00
15 changed files with 1050 additions and 35 deletions

View File

@@ -73,6 +73,28 @@ 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)
// })
//
//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

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

View File

@@ -599,3 +599,28 @@ func ChainFirstLeft[A, EA, EB, B any](f Kleisli[EB, EA, B]) Operator[EA, A, A] {
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

@@ -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[int](0)
})
res := recover(Left[int](fmt.Errorf("error")))()
assert.Equal(t, result.Of(0), res)
// Test Right value passes through unchanged
res = recover(Right[int](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[int](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[int](1)
}
return Left[int](err)
})
secondRecover := OrElse(func(err error) IOResult[int] {
if err.Error() == "error2" {
return Right[int](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

@@ -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,220 @@ 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 MonadChainLeft(ma, function.Flow2(f, Fold(function.Constant1[EB](ma), function.Constant1[B](ma))))
}
//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]) func(ReaderEither[R, EA, A]) ReaderEither[R, EA, A] {
return ChainLeft(func(e EA) ReaderEither[R, EA, A] {
ma := Left[R, A](e)
return MonadFold(f(e), function.Constant1[EB](ma), function.Constant1[B](ma))
})
}
//go:inline
func TapLeft[A, R, EA, EB, B any](f Kleisli[R, EB, EA, B]) func(ReaderEither[R, EA, A]) ReaderEither[R, EA, 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[E, L, A, B](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, int](42)
recover := OrElse[Config, string, string, int](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[Config, string, string, int](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[Config, ValidationError, AppError, int](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

@@ -684,14 +684,6 @@ func GetOrElse[R, E, A any](onLeft func(E) ReaderIO[R, A]) func(ReaderIOEither[R
return eithert.GetOrElse(readerio.MonadChain[R, either.Either[E, A], A], readerio.Of[R, A], onLeft)
}
// OrElse tries an alternative computation if the first one fails.
// The alternative can produce a different error type.
//
//go:inline
func OrElse[R, E1, A, E2 any](onLeft func(E1) ReaderIOEither[R, E2, A]) func(ReaderIOEither[R, E1, A]) ReaderIOEither[R, E2, A] {
return eithert.OrElse(readerio.MonadChain[R, either.Either[E1, A], either.Either[E2, A]], readerio.Of[R, either.Either[E2, A]], onLeft)
}
// OrLeft transforms the error using a ReaderIO if the computation fails.
// The success value is preserved unchanged.
//
@@ -829,6 +821,42 @@ func Read[E, A, R any](r R) func(ReaderIOEither[R, E, A]) IOEither[E, A] {
return reader.Read[IOEither[E, A]](r)
}
// MonadChainLeft chains a computation on the left (error) side of a ReaderIOEither.
// 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 [OrElse]. Use [ChainLeft] when
// emphasizing the monadic chaining perspective, and [OrElse] for error recovery semantics.
//
// Parameters:
// - fa: The input ReaderIOEither that may contain an error of type EA
// - f: A Kleisli function that takes an error of type EA and returns a ReaderIOEither with error type EB
//
// Returns:
// - A ReaderIOEither with the potentially transformed error type EB
//
// Example:
//
// type Config struct{ retryCount int }
// type NetworkError struct{ msg string }
// type SystemError struct{ code int }
//
// // Recover from network errors by retrying with config
// result := MonadChainLeft(
// Left[Config, string](NetworkError{"connection failed"}),
// func(ne NetworkError) readerioeither.ReaderIOEither[Config, SystemError, string] {
// return readerioeither.Asks[SystemError](func(cfg Config) ioeither.IOEither[SystemError, string] {
// if cfg.retryCount > 0 {
// return ioeither.Right[SystemError]("recovered")
// }
// return ioeither.Left[string](SystemError{500})
// })
// },
// )
//
//go:inline
func MonadChainLeft[R, EA, EB, A any](fa ReaderIOEither[R, EA, A], f Kleisli[R, EB, EA, A]) ReaderIOEither[R, EB, A] {
return readert.MonadChain(
@@ -838,6 +866,44 @@ func MonadChainLeft[R, EA, EB, A any](fa ReaderIOEither[R, EA, A], f Kleisli[R,
)
}
// ChainLeft is the curried version of [MonadChainLeft].
// It returns a function that chains a computation on the left (error) side of a ReaderIOEither.
//
// 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 [OrElse]. They are different names for the same operation.
// Use [ChainLeft] when emphasizing the monadic chaining perspective on the error channel,
// and [OrElse] when emphasizing error recovery/fallback semantics.
//
// Parameters:
// - f: A Kleisli function that takes an error of type EA and returns a ReaderIOEither with error type EB
//
// Returns:
// - A function that transforms a ReaderIOEither with error type EA to one with error type EB
//
// Example:
//
// type Config struct{ fallbackService string }
//
// // Create a reusable error handler with config access
// recoverFromNetworkError := ChainLeft(func(err string) readerioeither.ReaderIOEither[Config, string, int] {
// if strings.Contains(err, "network") {
// return readerioeither.Asks[string](func(cfg Config) ioeither.IOEither[string, int] {
// return ioeither.TryCatch(
// func() (int, error) { return callService(cfg.fallbackService) },
// func(e error) string { return e.Error() },
// )
// })
// }
// return readerioeither.Left[Config, int](err)
// })
//
// result := F.Pipe1(
// readerioeither.Left[Config, int]("network timeout"),
// recoverFromNetworkError,
// )(Config{fallbackService: "backup"})()
//
//go:inline
func ChainLeft[R, EA, EB, A any](f Kleisli[R, EB, EA, A]) func(ReaderIOEither[R, EA, A]) ReaderIOEither[R, EB, A] {
return readert.Chain[ReaderIOEither[R, EA, A]](
@@ -910,3 +976,33 @@ func Delay[R, E, A any](delay time.Duration) Operator[R, E, A, A] {
func After[R, E, A any](timestamp time.Time) Operator[R, E, A, A] {
return function.Bind2nd(function.Flow2[ReaderIOEither[R, E, A]], io.After[Either[E, A]](timestamp))
}
// OrElse recovers from a Left (error) by providing an alternative IO computation with access to the reader context.
// If the ReaderIOEither is Right, it returns the value unchanged.
// If the ReaderIOEither is Left, it applies the provided function to the error value,
// which returns a new ReaderIOEither that replaces the original.
//
// This is useful for error recovery, fallback logic, or chaining alternative IO computations
// that need access to configuration or dependencies. The error type can be widened from E1 to E2.
//
// Example:
//
// type Config struct{ retryLimit int }
//
// // Recover with IO operation using config
// recover := readerioeither.OrElse(func(err error) readerioeither.ReaderIOEither[Config, error, int] {
// if err.Error() == "retryable" {
// return readerioeither.Asks[error](func(cfg Config) ioeither.IOEither[error, int] {
// if cfg.retryLimit > 0 {
// return ioeither.Right[error](42)
// }
// return ioeither.Left[int](err)
// })
// }
// return readerioeither.Left[Config, int](err)
// })
//
//go:inline
func OrElse[R, E1, E2, A any](onLeft Kleisli[R, E2, E1, A]) Kleisli[R, E2, ReaderIOEither[R, E1, A], A] {
return Fold(onLeft, Of[R, E2, A])
}

View File

@@ -23,6 +23,7 @@ import (
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
IOE "github.com/IBM/fp-go/v2/ioeither"
R "github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
"github.com/stretchr/testify/assert"
@@ -77,3 +78,154 @@ func TestChainReaderK(t *testing.T) {
assert.Equal(t, E.Right[error]("1"), g(context.Background())())
}
func TestOrElseWFunc(t *testing.T) {
type Config struct {
retryEnabled bool
}
// Test with Right - should pass through unchanged
t.Run("Right passes through", func(t *testing.T) {
rioe := Right[Config, string](42)
handler := OrElse(func(err string) ReaderIOEither[Config, int, int] {
return Left[Config, int](999)
})
result := handler(rioe)(Config{retryEnabled: true})()
assert.Equal(t, E.Right[int](42), result)
})
// Test with Left - error type widening
t.Run("Left with error type widening", func(t *testing.T) {
rioe := Left[Config, int]("network error")
handler := OrElse(func(err string) ReaderIOEither[Config, int, int] {
return func(cfg Config) IOEither[int, int] {
if cfg.retryEnabled {
return IOE.Right[int](100)
}
return IOE.Left[int](404)
}
})
result := handler(rioe)(Config{retryEnabled: true})()
assert.Equal(t, E.Right[int](100), result)
})
}
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) ReaderIOEither[Config, int, int] {
return Left[Config, int](999)
}),
)
result := g(Config{errorCode: 500})()
assert.Equal(t, E.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) ReaderIOEither[Config, int, int] {
return func(cfg Config) IOEither[int, int] {
return IOE.Left[int](cfg.errorCode)
}
}),
)
result := g(Config{errorCode: 500})()
assert.Equal(t, E.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) ReaderIOEither[Config, int, int] {
if err == "recoverable" {
return Right[Config, int](999)
}
return Left[Config, int](0)
}),
)
result := g(Config{errorCode: 500})()
assert.Equal(t, E.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) ReaderIOEither[Config, int, string] {
logged = true
return Right[Config, int]("logged")
}),
)
result := g(Config{logEnabled: true})()
assert.Equal(t, E.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) ReaderIOEither[Config, int, string] {
return func(cfg Config) IOEither[int, string] {
if cfg.logEnabled {
logged = true
}
return IOE.Right[int]("side effect done")
}
}),
)
result := g(Config{logEnabled: true})()
assert.Equal(t, E.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) ReaderIOEither[Config, int, string] {
return Left[Config, string](999) // Side effect fails
}),
)
result := g(Config{logEnabled: true})()
assert.Equal(t, E.Left[int]("original error"), result)
})
}
func TestTapLeft(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) ReaderIOEither[Config, string, int] {
sideEffectRan = true
return Right[Config, string](0)
}),
)
result := g(Config{})()
assert.Equal(t, E.Left[int]("error"), result)
assert.True(t, sideEffectRan)
}

View File

@@ -397,22 +397,6 @@ func TestGetOrElse(t *testing.T) {
assert.Equal(t, 0, resultLeft(ctx)())
}
func TestOrElse(t *testing.T) {
ctx := testContext{value: 10}
// Test Right case
resultRight := OrElse(func(e error) ReaderIOEither[testContext, string, int] {
return Left[testContext, int]("alternative")
})(Of[testContext, error](42))
assert.Equal(t, E.Right[string](42), resultRight(ctx)())
// Test Left case
resultLeft := OrElse(func(e error) ReaderIOEither[testContext, string, int] {
return Of[testContext, string](99)
})(Left[testContext, int](errors.New("test")))
assert.Equal(t, E.Right[string](99), resultLeft(ctx)())
}
func TestMonadBiMap(t *testing.T) {
ctx := testContext{value: 10}

View File

@@ -648,10 +648,9 @@ func GetOrElse[R, A any](onLeft func(error) ReaderIO[R, A]) func(ReaderIOResult[
}
// OrElse tries an alternative computation if the first one fails.
// The alternative can produce a different error type.
//
//go:inline
func OrElse[R, A, E any](onLeft func(error) RIOE.ReaderIOEither[R, E, A]) func(ReaderIOResult[R, A]) RIOE.ReaderIOEither[R, E, A] {
func OrElse[R, A any](onLeft Kleisli[R, error, A]) Operator[R, A, A] {
return RIOE.OrElse(onLeft)
}

View File

@@ -768,3 +768,41 @@ func TestWithLock(t *testing.T) {
assert.Equal(t, result.Of(42), res(ctx)())
assert.True(t, unlocked)
}
func TestOrElse(t *testing.T) {
type Config struct {
fallbackValue int
}
ctx := Config{fallbackValue: 99}
// Test OrElse with Right - should pass through unchanged
rightValue := Of[Config](42)
recover := OrElse[Config, int](func(err error) ReaderIOResult[Config, int] {
return Left[Config, int](errors.New("should not be called"))
})
res := recover(rightValue)(ctx)()
assert.Equal(t, result.Of(42), res)
// Test OrElse with Left - should recover with fallback
leftValue := Left[Config, int](errors.New("not found"))
recoverWithFallback := OrElse[Config, int](func(err error) ReaderIOResult[Config, int] {
if err.Error() == "not found" {
return func(cfg Config) IOResult[int] {
return func() result.Result[int] {
return result.Of(cfg.fallbackValue)
}
}
}
return Left[Config, int](err)
})
res = recoverWithFallback(leftValue)(ctx)()
assert.Equal(t, result.Of(99), res)
// Test OrElse with Left - should propagate other errors
leftValue = Left[Config, int](errors.New("fatal error"))
res = recoverWithFallback(leftValue)(ctx)()
assert.True(t, result.IsLeft(res))
val, err := result.UnwrapError(res)
assert.Equal(t, 0, val)
assert.Equal(t, "fatal error", err.Error())
}