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

Compare commits

..

7 Commits

Author SHA1 Message Date
lif
6a6b982779 feat: Add OrElse to ioeither for error recovery (#148)
* feat: add OrElse to ioeither for error recovery

Add OrElse function to both v1 and v2 ioeither packages for error recovery.
This allows recovering from a Left value by applying a function to the error
and returning a new IOEither, consistent with the Either package's API.

- Add OrElse to ioeither/generic/ioeither.go
- Add OrElse wrapper to ioeither/ioeither.go
- Add OrElse to v2/ioeither/generic/ioeither.go
- Add OrElse to v2/ioeither/ioeither.go
- Add comprehensive tests for both v1 and v2

Closes #146

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: majiayu000 <1835304752@qq.com>

* chore(v2): drop ioeither OrElse addition

---------

Signed-off-by: majiayu000 <1835304752@qq.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 16:41:13 +01:00
Carsten Leue
9d31752887 Rewrite the Retry logic based on Trampoline (#149)
* fix: implement retry via tail rec

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: base retry on Trampoline

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: refactor retry

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

---------

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-02 15:43:51 +01:00
Carsten Leue
14b52568b5 Add OrElse consistently and improve docs (#147)
* fix: OrElse

Signed-off-by: Carsten Leue <carsten.leue@de.ibm.com>

* fix: improve tests

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: FilterOrElse

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: tests and doc

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: add sample

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: add tests for CopyFile

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: signature of Close

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

---------

Signed-off-by: Carsten Leue <carsten.leue@de.ibm.com>
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-31 15:59:10 +01:00
Dr. Carsten Leue
49227551b6 fix: more iter methods
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-22 15:03:47 +01:00
Dr. Carsten Leue
69691e9e70 fix: iterators
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-20 16:38:36 +01:00
Dr. Carsten Leue
d3c466bfb7 fix: some cleanup
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-19 13:18:49 +01:00
Dr. Carsten Leue
a6c6ea804f fix: overhaul record
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-18 18:32:45 +01:00
224 changed files with 17285 additions and 938 deletions

View File

@@ -369,6 +369,11 @@ func ToIOOption[GA ~func() O.Option[A], GEA ~func() ET.Either[E, A], E, A any](i
)
}
// OrElse returns the original IOEither if it is a Right, otherwise it applies the given function to the error and returns the result.
func OrElse[GA ~func() ET.Either[E, A], E, A any](onLeft func(E) GA) func(GA) GA {
return eithert.OrElse(IO.MonadChain[GA, GA, ET.Either[E, A], ET.Either[E, A]], IO.Of[GA, ET.Either[E, A]], onLeft)
}
func FromIOOption[GEA ~func() ET.Either[E, A], GA ~func() O.Option[A], E, A any](onNone func() E) func(ioo GA) GEA {
return IO.Map[GA, GEA](ET.FromOption[A](onNone))
}

View File

@@ -266,6 +266,11 @@ func Alt[E, A any](second L.Lazy[IOEither[E, A]]) func(IOEither[E, A]) IOEither[
return G.Alt(second)
}
// OrElse returns the original IOEither if it is a Right, otherwise it applies the given function to the error and returns the result.
func OrElse[E, A any](onLeft func(E) IOEither[E, A]) func(IOEither[E, A]) IOEither[E, A] {
return G.OrElse[IOEither[E, A]](onLeft)
}
func MonadFlap[E, B, A any](fab IOEither[E, func(A) B], a A) IOEither[E, B] {
return G.MonadFlap[IOEither[E, func(A) B], IOEither[E, B]](fab, a)
}

View File

@@ -134,3 +134,44 @@ func TestApSecond(t *testing.T) {
assert.Equal(t, E.Of[error]("b"), x())
}
func TestOrElse(t *testing.T) {
// Test that OrElse recovers from a Left
recover := OrElse(func(err string) IOEither[string, int] {
return Right[string](42)
})
// When input is Left, should recover
leftResult := F.Pipe1(
Left[int]("error"),
recover,
)
assert.Equal(t, E.Right[string](42), leftResult())
// When input is Right, should pass through unchanged
rightResult := F.Pipe1(
Right[string](100),
recover,
)
assert.Equal(t, E.Right[string](100), rightResult())
// Test that OrElse can also return a Left (propagate different error)
recoverOrFail := OrElse(func(err string) IOEither[string, int] {
if err == "recoverable" {
return Right[string](0)
}
return Left[int]("unrecoverable: " + err)
})
recoverable := F.Pipe1(
Left[int]("recoverable"),
recoverOrFail,
)
assert.Equal(t, E.Right[string](0), recoverable())
unrecoverable := F.Pipe1(
Left[int]("fatal"),
recoverOrFail,
)
assert.Equal(t, E.Left[int]("unrecoverable: fatal"), unrecoverable())
}

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

@@ -3,13 +3,18 @@ package nonempty
import "github.com/IBM/fp-go/v2/option"
type (
// NonEmptyArray represents an array with at least one element
// NonEmptyArray represents an array that is guaranteed to have at least one element.
// This provides compile-time safety for operations that require non-empty collections.
NonEmptyArray[A any] []A
// Kleisli represents a Kleisli arrow for the NonEmptyArray monad.
// It's a function from A to NonEmptyArray[B], used for composing operations that produce non-empty arrays.
Kleisli[A, B any] = func(A) NonEmptyArray[B]
// Operator represents a function that transforms one NonEmptyArray into another.
// It takes a NonEmptyArray[A] and produces a NonEmptyArray[B].
Operator[A, B any] = Kleisli[NonEmptyArray[A], B]
// Option represents an optional value that may or may not be present.
Option[A any] = option.Option[A]
)

View File

@@ -3,7 +3,14 @@ package array
import "github.com/IBM/fp-go/v2/option"
type (
Kleisli[A, B any] = func(A) []B
// Kleisli represents a Kleisli arrow for arrays.
// It's a function from A to []B, used for composing operations that produce arrays.
Kleisli[A, B any] = func(A) []B
// Operator represents a function that transforms one array into another.
// It takes a []A and produces a []B.
Operator[A, B any] = Kleisli[[]A, B]
Option[A any] = option.Option[A]
// Option represents an optional value that may or may not be present.
Option[A any] = option.Option[A]
)

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"
)
@@ -98,7 +99,7 @@ func Example_resultAssertions() {
var t *testing.T // placeholder for example
// Assert success
successResult := result.Of[int](42)
successResult := result.Of(42)
assert.Success(successResult)(t)
// Assert failure
@@ -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

@@ -12,11 +12,24 @@ import (
)
type (
Result[T any] = result.Result[T]
Reader = reader.Reader[*testing.T, bool]
Kleisli[T any] = reader.Reader[T, Reader]
Predicate[T any] = predicate.Predicate[T]
Lens[S, T any] = lens.Lens[S, T]
// Result represents a computation that may fail with an error.
Result[T any] = result.Result[T]
// Reader represents a test assertion that depends on a testing.T context and returns a boolean.
Reader = reader.Reader[*testing.T, bool]
// Kleisli represents a function that produces a test assertion Reader from a value of type T.
Kleisli[T any] = reader.Reader[T, Reader]
// Predicate represents a function that tests a value of type T and returns a boolean.
Predicate[T any] = predicate.Predicate[T]
// Lens is a functional reference to a subpart of a data structure.
Lens[S, T any] = lens.Lens[S, T]
// Optional is an optic that focuses on a value that may or may not be present.
Optional[S, T any] = optional.Optional[S, T]
Prism[S, T any] = prism.Prism[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

@@ -18,5 +18,7 @@ package boolean
import "github.com/IBM/fp-go/v2/monoid"
type (
// Monoid represents a monoid structure for boolean values.
// A monoid provides an associative binary operation and an identity element.
Monoid = monoid.Monoid[bool]
)

View File

@@ -7,9 +7,14 @@ import (
)
type (
// Result represents a computation that may fail with an error.
// It's an alias for Either[error, T].
Result[T any] = result.Result[T]
// Prism is an optic that focuses on a case of a sum type.
// It provides a way to extract and construct values of a specific variant.
Prism[S, A any] = prism.Prism[S, A]
// Option represents an optional value that may or may not be present.
Option[T any] = option.Option[T]
)

View File

@@ -52,5 +52,7 @@ type (
// }
Consumer[A any] = func(A)
// Operator represents a function that transforms a Consumer[A] into a Consumer[B].
// This is useful for composing and adapting consumers to work with different types.
Operator[A, B any] = func(Consumer[A]) Consumer[B]
)

View File

@@ -21,7 +21,34 @@ import (
"github.com/IBM/fp-go/v2/result"
)
// withContext wraps an existing IOEither and performs a context check for cancellation before delegating
// WithContext wraps an IOResult and performs a context check for cancellation before executing.
// This ensures that if the context is already cancelled, the computation short-circuits immediately
// without executing the wrapped computation.
//
// This is useful for adding cancellation awareness to computations that might not check the context themselves.
//
// Type Parameters:
// - A: The type of the success value
//
// Parameters:
// - ctx: The context to check for cancellation
// - ma: The IOResult to wrap with context checking
//
// Returns:
// - An IOResult that checks for cancellation before executing
//
// Example:
//
// computation := func() Result[string] {
// // Long-running operation
// return result.Of("done")
// }
//
// ctx, cancel := context.WithCancel(context.Background())
// cancel() // Cancel immediately
//
// wrapped := WithContext(ctx, computation)
// result := wrapped() // Returns Left with context.Canceled error
func WithContext[A any](ctx context.Context, ma IOResult[A]) IOResult[A] {
return func() Result[A] {
if ctx.Err() != nil {

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

@@ -4,6 +4,65 @@ import (
RIO "github.com/IBM/fp-go/v2/readerio"
)
// Bracket ensures that a resource is properly acquired, used, and released, even if an error occurs.
// This implements the bracket pattern for safe resource management with [ReaderIO].
//
// The bracket pattern guarantees that:
// - The acquire action is executed first to obtain the resource
// - The use function is called with the acquired resource
// - The release function is always called with the resource and result, regardless of success or failure
// - The final result from the use function is returned
//
// This is particularly useful for managing resources like file handles, database connections,
// or locks that must be cleaned up properly.
//
// Type Parameters:
// - A: The type of the acquired resource
// - B: The type of the result produced by the use function
// - ANY: The type returned by the release function (typically ignored)
//
// Parameters:
// - acquire: A ReaderIO that acquires the resource
// - use: A Kleisli arrow that uses the resource and produces a result
// - release: A function that releases the resource, receiving both the resource and the result
//
// Returns:
// - A ReaderIO[B] that safely manages the resource lifecycle
//
// Example:
//
// // Acquire a file handle
// acquireFile := func(ctx context.Context) IO[*os.File] {
// return func() *os.File {
// f, _ := os.Open("data.txt")
// return f
// }
// }
//
// // Use the file
// readFile := func(f *os.File) ReaderIO[string] {
// return func(ctx context.Context) IO[string] {
// return func() string {
// data, _ := io.ReadAll(f)
// return string(data)
// }
// }
// }
//
// // Release the file
// closeFile := func(f *os.File, result string) ReaderIO[any] {
// return func(ctx context.Context) IO[any] {
// return func() any {
// f.Close()
// return nil
// }
// }
// }
//
// // Safely read file with automatic cleanup
// safeRead := Bracket(acquireFile, readFile, closeFile)
// result := safeRead(context.Background())()
//
//go:inline
func Bracket[
A, B, ANY any](

View File

@@ -2,11 +2,63 @@ package readerio
import "github.com/IBM/fp-go/v2/io"
// ChainConsumer chains a consumer function into a ReaderIO computation, discarding the original value.
// This is useful for performing side effects (like logging or metrics) that consume a value
// but don't produce a meaningful result.
//
// The consumer is executed for its side effects, and the computation returns an empty struct.
//
// Type Parameters:
// - A: The type of value to consume
//
// Parameters:
// - c: A consumer function that performs side effects on the value
//
// Returns:
// - An Operator that chains the consumer and returns struct{}
//
// Example:
//
// logUser := func(u User) {
// log.Printf("Processing user: %s", u.Name)
// }
//
// pipeline := F.Pipe2(
// fetchUser(123),
// ChainConsumer(logUser),
// )
//
//go:inline
func ChainConsumer[A any](c Consumer[A]) Operator[A, struct{}] {
return ChainIOK(io.FromConsumerK(c))
}
// ChainFirstConsumer chains a consumer function into a ReaderIO computation, preserving the original value.
// This is useful for performing side effects (like logging or metrics) while passing the value through unchanged.
//
// The consumer is executed for its side effects, but the original value is returned.
//
// Type Parameters:
// - A: The type of value to consume and return
//
// Parameters:
// - c: A consumer function that performs side effects on the value
//
// Returns:
// - An Operator that chains the consumer and returns the original value
//
// Example:
//
// logUser := func(u User) {
// log.Printf("User: %s", u.Name)
// }
//
// pipeline := F.Pipe3(
// fetchUser(123),
// ChainFirstConsumer(logUser), // Logs but passes user through
// Map(func(u User) string { return u.Email }),
// )
//
//go:inline
func ChainFirstConsumer[A any](c Consumer[A]) Operator[A, A] {
return ChainFirstIOK(io.FromConsumerK(c))

View File

@@ -7,11 +7,108 @@ import (
RIO "github.com/IBM/fp-go/v2/readerio"
)
// SequenceReader transforms a ReaderIO containing a Reader into a Reader containing a ReaderIO.
// This "flips" the nested structure, allowing you to provide the Reader's environment first,
// then get a ReaderIO that can be executed with a context.
//
// Type transformation:
//
// From: ReaderIO[Reader[R, A]]
// = func(context.Context) func() func(R) A
//
// To: Reader[R, ReaderIO[A]]
// = func(R) func(context.Context) func() A
//
// This is useful for point-free style programming where you want to partially apply
// the Reader's environment before dealing with the context.
//
// Type Parameters:
// - R: The environment type that the Reader depends on
// - A: The value type
//
// Parameters:
// - ma: A ReaderIO containing a Reader
//
// Returns:
// - A Reader that produces a ReaderIO when given an environment
//
// Example:
//
// type Config struct {
// Timeout int
// }
//
// // A computation that produces a Reader
// getMultiplier := func(ctx context.Context) IO[func(Config) int] {
// return func() func(Config) int {
// return func(cfg Config) int {
// return cfg.Timeout * 2
// }
// }
// }
//
// // Sequence it to apply Config first
// sequenced := SequenceReader[Config, int](getMultiplier)
// cfg := Config{Timeout: 30}
// result := sequenced(cfg)(context.Background())() // Returns 60
//
//go:inline
func SequenceReader[R, A any](ma ReaderIO[Reader[R, A]]) Reader[R, ReaderIO[A]] {
return RIO.SequenceReader(ma)
}
// TraverseReader applies a Reader-based transformation to a ReaderIO, introducing a new environment dependency.
//
// This function takes a Reader-based Kleisli arrow and returns a function that can transform
// a ReaderIO. The result allows you to provide the Reader's environment (R) first, which then
// produces a ReaderIO that depends on the context.
//
// Type transformation:
//
// From: ReaderIO[A]
// = func(context.Context) func() A
//
// With: reader.Kleisli[R, A, B]
// = func(A) func(R) B
//
// To: func(ReaderIO[A]) func(R) ReaderIO[B]
// = func(ReaderIO[A]) func(R) func(context.Context) func() B
//
// This enables transforming values within a ReaderIO using environment-dependent logic.
//
// Type Parameters:
// - R: The environment type that the Reader depends on
// - A: The input value type
// - B: The output value type
//
// Parameters:
// - f: A Reader-based Kleisli arrow that transforms A to B using environment R
//
// Returns:
// - A function that takes a ReaderIO[A] and returns a function from R to ReaderIO[B]
//
// Example:
//
// type Config struct {
// Multiplier int
// }
//
// // A Reader-based transformation
// multiply := func(x int) func(Config) int {
// return func(cfg Config) int {
// return x * cfg.Multiplier
// }
// }
//
// // Apply TraverseReader
// traversed := TraverseReader[Config, int, int](multiply)
// computation := Of(10)
// result := traversed(computation)
//
// // Provide Config to get final result
// cfg := Config{Multiplier: 5}
// finalResult := result(cfg)(context.Background())() // Returns 50
//
//go:inline
func TraverseReader[R, A, B any](
f reader.Kleisli[R, A, B],

View File

@@ -7,6 +7,40 @@ import (
"github.com/IBM/fp-go/v2/logging"
)
// SLogWithCallback creates a Kleisli arrow that logs a value with a custom logger and log level.
// The value is logged and then passed through unchanged, making this useful for debugging
// and monitoring values as they flow through a ReaderIO computation.
//
// Type Parameters:
// - A: The type of value to log and pass through
//
// Parameters:
// - logLevel: The slog.Level to use for logging (e.g., slog.LevelInfo, slog.LevelDebug)
// - cb: Callback function to retrieve the *slog.Logger from the context
// - message: A descriptive message to include in the log entry
//
// Returns:
// - A Kleisli arrow that logs the value and returns it unchanged
//
// Example:
//
// getMyLogger := func(ctx context.Context) *slog.Logger {
// if logger := ctx.Value("logger"); logger != nil {
// return logger.(*slog.Logger)
// }
// return slog.Default()
// }
//
// debugLog := SLogWithCallback[User](
// slog.LevelDebug,
// getMyLogger,
// "Processing user",
// )
//
// pipeline := F.Pipe2(
// fetchUser(123),
// Chain(debugLog),
// )
func SLogWithCallback[A any](
logLevel slog.Level,
cb func(context.Context) *slog.Logger,
@@ -23,6 +57,34 @@ func SLogWithCallback[A any](
}
}
// SLog creates a Kleisli arrow that logs a value at Info level and passes it through unchanged.
// This is a convenience wrapper around SLogWithCallback with standard settings.
//
// The value is logged with the provided message and then returned unchanged, making this
// useful for debugging and monitoring values in a ReaderIO computation pipeline.
//
// Type Parameters:
// - A: The type of value to log and pass through
//
// Parameters:
// - message: A descriptive message to include in the log entry
//
// Returns:
// - A Kleisli arrow that logs the value at Info level and returns it unchanged
//
// Example:
//
// pipeline := F.Pipe3(
// fetchUser(123),
// Chain(SLog[User]("Fetched user")),
// Map(func(u User) string { return u.Name }),
// Chain(SLog[string]("Extracted name")),
// )
//
// result := pipeline(context.Background())()
// // Logs: "Fetched user" value={ID:123 Name:"Alice"}
// // Logs: "Extracted name" value="Alice"
//
//go:inline
func SLog[A any](message string) Kleisli[A, A] {
return SLogWithCallback[A](slog.LevelInfo, logging.GetLoggerFromContext, message)

View File

@@ -19,6 +19,67 @@ import (
"github.com/IBM/fp-go/v2/readerio"
)
// TailRec implements stack-safe tail recursion for the ReaderIO monad.
//
// This function enables recursive computations that depend on a [context.Context] and
// perform side effects, without risking stack overflow. It uses an iterative loop to
// execute the recursion, making it safe for deep or unbounded recursion.
//
// The function takes a Kleisli arrow that returns Trampoline[A, B]:
// - Bounce(A): Continue recursion with the new state A
// - Land(B): Terminate recursion and return the final result B
//
// Type Parameters:
// - A: The state type that changes during recursion
// - B: The final result type when recursion terminates
//
// Parameters:
// - f: A Kleisli arrow (A => ReaderIO[Trampoline[A, B]]) that controls recursion flow
//
// Returns:
// - A Kleisli arrow (A => ReaderIO[B]) that executes the recursion safely
//
// Example - Countdown:
//
// countdownStep := func(n int) ReaderIO[tailrec.Trampoline[int, string]] {
// return func(ctx context.Context) IO[tailrec.Trampoline[int, string]] {
// return func() tailrec.Trampoline[int, string] {
// if n <= 0 {
// return tailrec.Land[int]("Done!")
// }
// return tailrec.Bounce[string](n - 1)
// }
// }
// }
//
// countdown := TailRec(countdownStep)
// result := countdown(10)(context.Background())() // Returns "Done!"
//
// Example - Sum with context:
//
// type SumState struct {
// numbers []int
// total int
// }
//
// sumStep := func(state SumState) ReaderIO[tailrec.Trampoline[SumState, int]] {
// return func(ctx context.Context) IO[tailrec.Trampoline[SumState, int]] {
// return func() tailrec.Trampoline[SumState, int] {
// if len(state.numbers) == 0 {
// return tailrec.Land[SumState](state.total)
// }
// return tailrec.Bounce[int](SumState{
// numbers: state.numbers[1:],
// total: state.total + state.numbers[0],
// })
// }
// }
// }
//
// sum := TailRec(sumStep)
// result := sum(SumState{numbers: []int{1, 2, 3, 4, 5}})(context.Background())()
// // Returns 15, safe even for very large slices
//
//go:inline
func TailRec[A, B any](f Kleisli[A, Trampoline[A, B]]) Kleisli[A, B] {
return readerio.TailRec(f)

View File

@@ -20,20 +20,85 @@ import (
RG "github.com/IBM/fp-go/v2/retry/generic"
)
// Retrying retries a ReaderIO computation according to a retry policy.
//
// This function implements a retry mechanism for operations that depend on a [context.Context]
// and perform side effects (IO). The retry loop continues until one of the following occurs:
// - The action succeeds and the check function returns false (no retry needed)
// - The retry policy returns None (retry limit reached)
// - The check function returns false (indicating success or a non-retryable condition)
//
// Type Parameters:
// - A: The type of the value produced by the action
//
// Parameters:
//
// - policy: A RetryPolicy that determines when and how long to wait between retries.
// The policy receives a RetryStatus on each iteration and returns an optional delay.
// If it returns None, retrying stops. Common policies include LimitRetries,
// ExponentialBackoff, and CapDelay from the retry package.
//
// - action: A Kleisli arrow that takes a RetryStatus and returns a ReaderIO[A].
// This function is called on each retry attempt and receives information about the
// current retry state (iteration number, cumulative delay, etc.).
//
// - check: A predicate function that examines the result A and returns true if the
// operation should be retried, or false if it should stop. This allows you to
// distinguish between retryable conditions and successful/permanent results.
//
// Returns:
// - A ReaderIO[A] that, when executed with a context, will perform the retry logic
// and return the final result.
//
// Example:
//
// // Create a retry policy: exponential backoff with a cap, limited to 5 retries
// policy := M.Concat(
// retry.LimitRetries(5),
// retry.CapDelay(10*time.Second, retry.ExponentialBackoff(100*time.Millisecond)),
// )(retry.Monoid)
//
// // Action that fetches data, with retry status information
// fetchData := func(status retry.RetryStatus) ReaderIO[string] {
// return func(ctx context.Context) IO[string] {
// return func() string {
// // Simulate an operation that might fail
// if status.IterNumber < 3 {
// return "" // Empty result indicates failure
// }
// return "success"
// }
// }
// }
//
// // Check function: retry if result is empty
// shouldRetry := func(s string) bool {
// return s == ""
// }
//
// // Create the retrying computation
// retryingFetch := Retrying(policy, fetchData, shouldRetry)
//
// // Execute
// ctx := context.Background()
// result := retryingFetch(ctx)() // Returns "success" after 3 attempts
//
//go:inline
func Retrying[A any](
policy retry.RetryPolicy,
action Kleisli[retry.RetryStatus, A],
check func(A) bool,
check Predicate[A],
) ReaderIO[A] {
// get an implementation for the types
return RG.Retrying(
Chain[A, A],
Chain[retry.RetryStatus, A],
Of[A],
Chain[A, Trampoline[retry.RetryStatus, A]],
Map[retry.RetryStatus, Trampoline[retry.RetryStatus, A]],
Of[Trampoline[retry.RetryStatus, A]],
Of[retry.RetryStatus],
Delay[retry.RetryStatus],
TailRec,
policy,
action,
check,

View File

@@ -22,6 +22,7 @@ import (
"github.com/IBM/fp-go/v2/either"
"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/readerio"
"github.com/IBM/fp-go/v2/tailrec"
@@ -75,4 +76,6 @@ type (
Either[E, A any] = either.Either[E, A]
Trampoline[B, L any] = tailrec.Trampoline[B, L]
Predicate[A any] = predicate.Predicate[A]
)

View File

@@ -42,6 +42,42 @@ func WithContext[A any](ma ReaderIOResult[A]) ReaderIOResult[A] {
}
}
// WithContextK wraps a Kleisli arrow with context cancellation checking.
// This ensures that the computation checks for context cancellation before executing,
// providing a convenient way to add cancellation awareness to Kleisli arrows.
//
// This is particularly useful when composing multiple Kleisli arrows where each step
// should respect context cancellation.
//
// Type Parameters:
// - A: The input type of the Kleisli arrow
// - B: The output type of the Kleisli arrow
//
// Parameters:
// - f: The Kleisli arrow to wrap with context checking
//
// Returns:
// - A Kleisli arrow that checks for cancellation before executing
//
// Example:
//
// fetchUser := func(id int) ReaderIOResult[User] {
// return func(ctx context.Context) IOResult[User] {
// return func() Result[User] {
// // Long-running operation
// return result.Of(User{ID: id})
// }
// }
// }
//
// // Wrap with context checking
// safeFetch := WithContextK(fetchUser)
//
// // If context is cancelled, returns immediately without executing fetchUser
// ctx, cancel := context.WithCancel(context.Background())
// cancel() // Cancel immediately
// result := safeFetch(123)(ctx)() // Returns context.Canceled error
//
//go:inline
func WithContextK[A, B any](f Kleisli[A, B]) Kleisli[A, B] {
return F.Flow2(

View File

@@ -2,11 +2,61 @@ package readerioresult
import "github.com/IBM/fp-go/v2/io"
// ChainConsumer chains a consumer function into a ReaderIOResult computation, discarding the original value.
// This is useful for performing side effects (like logging or metrics) that consume a value
// but don't produce a meaningful result. The computation continues with an empty struct.
//
// Type Parameters:
// - A: The type of value to consume
//
// Parameters:
// - c: A consumer function that performs side effects on the value
//
// Returns:
// - An Operator that chains the consumer and returns struct{}
//
// Example:
//
// logUser := func(u User) {
// log.Printf("Processing user: %s", u.Name)
// }
//
// pipeline := F.Pipe2(
// fetchUser(123),
// ChainConsumer(logUser),
// )
//
//go:inline
func ChainConsumer[A any](c Consumer[A]) Operator[A, struct{}] {
return ChainIOK(io.FromConsumerK(c))
}
// ChainFirstConsumer chains a consumer function into a ReaderIOResult computation, preserving the original value.
// This is useful for performing side effects (like logging or metrics) while passing the value through unchanged.
//
// The consumer is executed for its side effects, but the original value is returned.
//
// Type Parameters:
// - A: The type of value to consume and return
//
// Parameters:
// - c: A consumer function that performs side effects on the value
//
// Returns:
// - An Operator that chains the consumer and returns the original value
//
// Example:
//
// logUser := func(u User) {
// log.Printf("User: %s", u.Name)
// }
//
// pipeline := F.Pipe3(
// fetchUser(123),
// ChainFirstConsumer(logUser), // Logs but passes user through
// Map(func(u User) string { return u.Email }),
// )
//
//go:inline
func ChainFirstConsumer[A any](c Consumer[A]) Operator[A, A] {
return ChainFirstIOK(io.FromConsumerK(c))

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

@@ -121,7 +121,7 @@ import (
func Retrying[A any](
policy R.RetryPolicy,
action Kleisli[R.RetryStatus, A],
check func(Result[A]) bool,
check Predicate[Result[A]],
) ReaderIOResult[A] {
// delayWithCancel implements a context-aware delay mechanism for retry operations.
@@ -165,12 +165,14 @@ func Retrying[A any](
// get an implementation for the types
return RG.Retrying(
RIO.Chain[Result[A], Result[A]],
RIO.Chain[R.RetryStatus, Result[A]],
RIO.Of[Result[A]],
RIO.Chain[Result[A], Trampoline[R.RetryStatus, Result[A]]],
RIO.Map[R.RetryStatus, Trampoline[R.RetryStatus, Result[A]]],
RIO.Of[Trampoline[R.RetryStatus, Result[A]]],
RIO.Of[R.RetryStatus],
delayWithCancel,
RIO.TailRec,
policy,
WithContextK(action),
check,

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

@@ -24,11 +24,81 @@ import (
RG "github.com/IBM/fp-go/v2/retry/generic"
)
// Retrying retries a ReaderResult computation according to a retry policy with context awareness.
//
// This function implements a retry mechanism for operations that depend on a [context.Context]
// and can fail (Result). It respects context cancellation, meaning that if the context is
// cancelled during retry delays, the operation will stop immediately and return the cancellation error.
//
// The retry loop will continue until one of the following occurs:
// - The action succeeds and the check function returns false (no retry needed)
// - The retry policy returns None (retry limit reached)
// - The check function returns false (indicating success or a non-retryable failure)
// - The context is cancelled (returns context.Canceled or context.DeadlineExceeded)
//
// Type Parameters:
// - A: The type of the success value
//
// Parameters:
//
// - policy: A RetryPolicy that determines when and how long to wait between retries.
// The policy receives a RetryStatus on each iteration and returns an optional delay.
// If it returns None, retrying stops. Common policies include LimitRetries,
// ExponentialBackoff, and CapDelay from the retry package.
//
// - action: A Kleisli arrow that takes a RetryStatus and returns a ReaderResult[A].
// This function is called on each retry attempt and receives information about the
// current retry state (iteration number, cumulative delay, etc.). The action depends
// on a context.Context and produces a Result[A].
//
// - check: A predicate function that examines the Result[A] and returns true if the
// operation should be retried, or false if it should stop. This allows you to
// distinguish between retryable failures (e.g., network timeouts) and permanent
// failures (e.g., invalid input).
//
// Returns:
// - A ReaderResult[A] that, when executed with a context, will perform the retry
// logic with context cancellation support and return the final result.
//
// Example:
//
// // Create a retry policy: exponential backoff with a cap, limited to 5 retries
// policy := M.Concat(
// retry.LimitRetries(5),
// retry.CapDelay(10*time.Second, retry.ExponentialBackoff(100*time.Millisecond)),
// )(retry.Monoid)
//
// // Action that fetches data
// fetchData := func(status retry.RetryStatus) ReaderResult[string] {
// return func(ctx context.Context) Result[string] {
// if ctx.Err() != nil {
// return result.Left[string](ctx.Err())
// }
// if status.IterNumber < 3 {
// return result.Left[string](fmt.Errorf("temporary error"))
// }
// return result.Of("success")
// }
// }
//
// // Check function: retry on any error except context cancellation
// shouldRetry := func(r Result[string]) bool {
// return result.IsLeft(r) && !errors.Is(result.GetLeft(r), context.Canceled)
// }
//
// // Create the retrying computation
// retryingFetch := Retrying(policy, fetchData, shouldRetry)
//
// // Execute with a cancellable context
// ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// defer cancel()
// finalResult := retryingFetch(ctx)
//
//go:inline
func Retrying[A any](
policy R.RetryPolicy,
action Kleisli[R.RetryStatus, A],
check func(Result[A]) bool,
check Predicate[Result[A]],
) ReaderResult[A] {
// delayWithCancel implements a context-aware delay mechanism for retry operations.
@@ -70,12 +140,14 @@ func Retrying[A any](
// get an implementation for the types
return RG.Retrying(
RD.Chain[context.Context, Result[A], Result[A]],
RD.Chain[context.Context, R.RetryStatus, Result[A]],
RD.Of[context.Context, Result[A]],
RD.Chain[context.Context, Result[A], Trampoline[R.RetryStatus, Result[A]]],
RD.Map[context.Context, R.RetryStatus, Trampoline[R.RetryStatus, Result[A]]],
RD.Of[context.Context, Trampoline[R.RetryStatus, Result[A]]],
RD.Of[context.Context, R.RetryStatus],
delayWithCancel,
RD.TailRec,
policy,
WithContextK(action),
check,

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

@@ -23,14 +23,15 @@ import (
IOR "github.com/IBM/fp-go/v2/ioresult"
L "github.com/IBM/fp-go/v2/lazy"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
R "github.com/IBM/fp-go/v2/record"
T "github.com/IBM/fp-go/v2/tuple"
"sync"
)
func providerToEntry(p Provider) T.Tuple2[string, ProviderFactory] {
return T.MakeTuple2(p.Provides().Id(), p.Factory())
func providerToEntry(p Provider) Entry[string, ProviderFactory] {
return pair.MakePair(p.Provides().Id(), p.Factory())
}
func itemProviderToMap(p Provider) map[string][]ProviderFactory {

View File

@@ -4,10 +4,12 @@ import (
"github.com/IBM/fp-go/v2/iooption"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/record"
)
type (
Option[T any] = option.Option[T]
IOResult[T any] = ioresult.IOResult[T]
IOOption[T any] = iooption.IOOption[T]
Option[T any] = option.Option[T]
IOResult[T any] = ioresult.IOResult[T]
IOOption[T any] = iooption.IOOption[T]
Entry[K comparable, V any] = record.Entry[K, V]
)

View File

@@ -4,12 +4,23 @@ import (
"github.com/IBM/fp-go/v2/context/ioresult"
"github.com/IBM/fp-go/v2/iooption"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/record"
"github.com/IBM/fp-go/v2/result"
)
type (
Option[T any] = option.Option[T]
Result[T any] = result.Result[T]
// Option represents an optional value that may or may not be present.
Option[T any] = option.Option[T]
// Result represents a computation that may fail with an error.
Result[T any] = result.Result[T]
// IOResult represents a synchronous computation that may fail with an error.
IOResult[T any] = ioresult.IOResult[T]
// IOOption represents a synchronous computation that may not produce a value.
IOOption[T any] = iooption.IOOption[T]
// Entry represents a key-value pair in a record/map structure.
Entry[K comparable, V any] = record.Entry[K, V]
)

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,36 @@ 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 represents a function that tests a value of type A and returns a boolean.
// It's commonly used for filtering and conditional operations.
Predicate[A any] = predicate.Predicate[A]
)

View File

@@ -37,6 +37,8 @@ type (
// var g endomorphism.Endomorphism[int] = increment
Endomorphism[A any] = func(A) A
// Kleisli represents a Kleisli arrow for endomorphisms.
// It's a function from A to Endomorphism[A], used for composing endomorphic operations.
Kleisli[A any] = func(A) Endomorphism[A]
// Operator represents a transformation from one endomorphism to another.

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

@@ -0,0 +1,120 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package readerresult implements a specialization of the Reader monad assuming a golang context as the context of the monad and a standard golang error
package readerresult
import (
"context"
"github.com/IBM/fp-go/v2/idiomatic/result"
)
// TailRec implements tail-recursive computation for ReaderResult with context cancellation support.
//
// TailRec takes a Kleisli function that returns Trampoline[A, B] and converts it into a stack-safe,
// tail-recursive computation. The function repeatedly applies the Kleisli until it produces a Land value.
//
// The implementation includes a short-circuit mechanism that checks for context cancellation on each
// iteration. If the context is canceled (ctx.Err() != nil), the computation immediately returns an
// error result containing the context's cause error, preventing unnecessary computation.
//
// Type Parameters:
// - A: The input type for the recursive step
// - B: The final result type
//
// Parameters:
// - f: A Kleisli function that takes an A and returns a ReaderResult containing Trampoline[A, B].
// When the result is Bounce(a), recursion continues with the new value 'a'.
// When the result is Land(b), recursion terminates with the final value 'b'.
//
// Returns:
// - A Kleisli function that performs the tail-recursive computation in a stack-safe manner.
//
// Behavior:
// - On each iteration, checks if the context has been canceled (short circuit)
// - If canceled, returns (zero value, context.Cause(ctx))
// - If the step returns an error, propagates the error as (zero value, error)
// - If the step returns Bounce(a), continues recursion with new value 'a'
// - If the step returns Land(b), terminates with success value (b, nil)
//
// Example - Factorial computation with context:
//
// type State struct {
// n int
// acc int
// }
//
// factorialStep := func(state State) ReaderResult[tailrec.Trampoline[State, int]] {
// return func(ctx context.Context) (tailrec.Trampoline[State, int], error) {
// if state.n <= 0 {
// return tailrec.Land[State](state.acc), nil
// }
// return tailrec.Bounce[int](State{state.n - 1, state.acc * state.n}), nil
// }
// }
//
// factorial := TailRec(factorialStep)
// result, err := factorial(State{5, 1})(ctx) // Returns (120, nil)
//
// Example - Context cancellation:
//
// ctx, cancel := context.WithCancel(context.Background())
// cancel() // Cancel immediately
//
// computation := TailRec(someStep)
// result, err := computation(initialValue)(ctx)
// // Returns (zero value, context.Cause(ctx)) without executing any steps
//
// Example - Error handling:
//
// errorStep := func(n int) ReaderResult[tailrec.Trampoline[int, int]] {
// return func(ctx context.Context) (tailrec.Trampoline[int, int], error) {
// if n == 5 {
// return tailrec.Trampoline[int, int]{}, errors.New("computation error")
// }
// if n <= 0 {
// return tailrec.Land[int](n), nil
// }
// return tailrec.Bounce[int](n - 1), nil
// }
// }
//
// computation := TailRec(errorStep)
// result, err := computation(10)(ctx) // Returns (0, errors.New("computation error"))
//
//go:inline
func TailRec[A, B any](f Kleisli[A, Trampoline[A, B]]) Kleisli[A, B] {
return func(a A) ReaderResult[B] {
initialReader := f(a)
return func(ctx context.Context) (B, error) {
rdr := initialReader
for {
// short circuit
if ctx.Err() != nil {
return result.Left[B](context.Cause(ctx))
}
rec, e := rdr(ctx)
if e != nil {
return result.Left[B](e)
}
if rec.Landed {
return result.Of(rec.Land)
}
rdr = f(rec.Bounce)
}
}
}
}

View File

@@ -0,0 +1,597 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerresult
import (
"context"
"errors"
"fmt"
"testing"
"time"
A "github.com/IBM/fp-go/v2/array"
TR "github.com/IBM/fp-go/v2/tailrec"
"github.com/stretchr/testify/assert"
)
// TestTailRecFactorial tests factorial computation with context
func TestTailRecFactorial(t *testing.T) {
type State struct {
n int
acc int
}
factorialStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
if state.n <= 0 {
return TR.Land[State](state.acc), nil
}
return TR.Bounce[int](State{state.n - 1, state.acc * state.n}), nil
}
}
factorial := TailRec(factorialStep)
result, err := factorial(State{5, 1})(context.Background())
assert.NoError(t, err)
assert.Equal(t, 120, result)
}
// TestTailRecFibonacci tests Fibonacci computation
func TestTailRecFibonacci(t *testing.T) {
type State struct {
n int
prev int
curr int
}
fibStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
if state.n <= 0 {
return TR.Land[State](state.curr), nil
}
return TR.Bounce[int](State{state.n - 1, state.curr, state.prev + state.curr}), nil
}
}
fib := TailRec(fibStep)
result, err := fib(State{10, 0, 1})(context.Background())
assert.NoError(t, err)
assert.Equal(t, 89, result) // 10th Fibonacci number
}
// TestTailRecCountdown tests countdown computation
func TestTailRecCountdown(t *testing.T) {
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
if n <= 0 {
return TR.Land[int](n), nil
}
return TR.Bounce[int](n - 1), nil
}
}
countdown := TailRec(countdownStep)
result, err := countdown(10)(context.Background())
assert.NoError(t, err)
assert.Equal(t, 0, result)
}
// TestTailRecImmediateTermination tests immediate termination (Land on first call)
func TestTailRecImmediateTermination(t *testing.T) {
immediateStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
return TR.Land[int](n * 2), nil
}
}
immediate := TailRec(immediateStep)
result, err := immediate(42)(context.Background())
assert.NoError(t, err)
assert.Equal(t, 84, result)
}
// TestTailRecStackSafety tests that TailRec handles large iterations without stack overflow
func TestTailRecStackSafety(t *testing.T) {
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
if n <= 0 {
return TR.Land[int](n), nil
}
return TR.Bounce[int](n - 1), nil
}
}
countdown := TailRec(countdownStep)
result, err := countdown(10000)(context.Background())
assert.NoError(t, err)
assert.Equal(t, 0, result)
}
// TestTailRecSumList tests summing a list
func TestTailRecSumList(t *testing.T) {
type State struct {
list []int
sum int
}
sumStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
if A.IsEmpty(state.list) {
return TR.Land[State](state.sum), nil
}
return TR.Bounce[int](State{state.list[1:], state.sum + state.list[0]}), nil
}
}
sumList := TailRec(sumStep)
result, err := sumList(State{[]int{1, 2, 3, 4, 5}, 0})(context.Background())
assert.NoError(t, err)
assert.Equal(t, 15, result)
}
// TestTailRecCollatzConjecture tests the Collatz conjecture
func TestTailRecCollatzConjecture(t *testing.T) {
collatzStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
if n <= 1 {
return TR.Land[int](n), nil
}
if n%2 == 0 {
return TR.Bounce[int](n / 2), nil
}
return TR.Bounce[int](3*n + 1), nil
}
}
collatz := TailRec(collatzStep)
result, err := collatz(10)(context.Background())
assert.NoError(t, err)
assert.Equal(t, 1, result)
}
// TestTailRecGCD tests greatest common divisor
func TestTailRecGCD(t *testing.T) {
type State struct {
a int
b int
}
gcdStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
if state.b == 0 {
return TR.Land[State](state.a), nil
}
return TR.Bounce[int](State{state.b, state.a % state.b}), nil
}
}
gcd := TailRec(gcdStep)
result, err := gcd(State{48, 18})(context.Background())
assert.NoError(t, err)
assert.Equal(t, 6, result)
}
// TestTailRecErrorPropagation tests that errors are properly propagated
func TestTailRecErrorPropagation(t *testing.T) {
expectedErr := errors.New("computation error")
errorStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
if n == 5 {
return TR.Trampoline[int, int]{}, expectedErr
}
if n <= 0 {
return TR.Land[int](n), nil
}
return TR.Bounce[int](n - 1), nil
}
}
computation := TailRec(errorStep)
result, err := computation(10)(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
assert.Equal(t, 0, result) // zero value
}
// TestTailRecContextCancellationImmediate tests short circuit when context is already canceled
func TestTailRecContextCancellationImmediate(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately before execution
stepExecuted := false
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
stepExecuted = true
if n <= 0 {
return TR.Land[int](n), nil
}
return TR.Bounce[int](n - 1), nil
}
}
countdown := TailRec(countdownStep)
result, err := countdown(10)(ctx)
// Should short circuit without executing any steps
assert.False(t, stepExecuted, "Step should not be executed when context is already canceled")
assert.Error(t, err)
assert.Equal(t, context.Canceled, err)
assert.Equal(t, 0, result) // zero value
}
// TestTailRecContextCancellationDuringExecution tests short circuit when context is canceled during execution
func TestTailRecContextCancellationDuringExecution(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
executionCount := 0
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
executionCount++
// Cancel after 3 iterations
if executionCount == 3 {
cancel()
}
if n <= 0 {
return TR.Land[int](n), nil
}
return TR.Bounce[int](n - 1), nil
}
}
countdown := TailRec(countdownStep)
result, err := countdown(100)(ctx)
// Should stop after cancellation
assert.Error(t, err)
assert.LessOrEqual(t, executionCount, 4, "Should stop shortly after cancellation")
assert.Equal(t, context.Canceled, err)
assert.Equal(t, 0, result) // zero value
}
// TestTailRecContextWithTimeout tests behavior with timeout context
func TestTailRecContextWithTimeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
executionCount := 0
slowStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
executionCount++
// Simulate slow computation
time.Sleep(20 * time.Millisecond)
if n <= 0 {
return TR.Land[int](n), nil
}
return TR.Bounce[int](n - 1), nil
}
}
computation := TailRec(slowStep)
result, err := computation(100)(ctx)
// Should timeout and return error
assert.Error(t, err)
assert.Less(t, executionCount, 100, "Should not complete all iterations due to timeout")
assert.Equal(t, context.DeadlineExceeded, err)
assert.Equal(t, 0, result) // zero value
}
// TestTailRecContextWithCause tests that context.Cause is properly returned
func TestTailRecContextWithCause(t *testing.T) {
customErr := errors.New("custom cancellation reason")
ctx, cancel := context.WithCancelCause(context.Background())
cancel(customErr)
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
if n <= 0 {
return TR.Land[int](n), nil
}
return TR.Bounce[int](n - 1), nil
}
}
countdown := TailRec(countdownStep)
result, err := countdown(10)(ctx)
assert.Error(t, err)
assert.Equal(t, customErr, err)
assert.Equal(t, 0, result) // zero value
}
// TestTailRecContextCancellationMultipleIterations tests that cancellation is checked on each iteration
func TestTailRecContextCancellationMultipleIterations(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
executionCount := 0
maxExecutions := 5
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
executionCount++
if executionCount == maxExecutions {
cancel()
}
if n <= 0 {
return TR.Land[int](n), nil
}
return TR.Bounce[int](n - 1), nil
}
}
countdown := TailRec(countdownStep)
result, err := countdown(1000)(ctx)
// Should detect cancellation on next iteration check
assert.Error(t, err)
// Should stop within 1-2 iterations after cancellation
assert.LessOrEqual(t, executionCount, maxExecutions+2)
assert.Equal(t, context.Canceled, err)
assert.Equal(t, 0, result) // zero value
}
// TestTailRecContextNotCanceled tests normal execution when context is not canceled
func TestTailRecContextNotCanceled(t *testing.T) {
ctx := context.Background()
executionCount := 0
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
executionCount++
if n <= 0 {
return TR.Land[int](n), nil
}
return TR.Bounce[int](n - 1), nil
}
}
countdown := TailRec(countdownStep)
result, err := countdown(10)(ctx)
assert.NoError(t, err)
assert.Equal(t, 11, executionCount) // 10, 9, 8, ..., 1, 0
assert.Equal(t, 0, result)
}
// TestTailRecPowerOfTwo tests computing power of 2
func TestTailRecPowerOfTwo(t *testing.T) {
type State struct {
exponent int
result int
target int
}
powerStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
if state.exponent >= state.target {
return TR.Land[State](state.result), nil
}
return TR.Bounce[int](State{state.exponent + 1, state.result * 2, state.target}), nil
}
}
power := TailRec(powerStep)
result, err := power(State{0, 1, 10})(context.Background())
assert.NoError(t, err)
assert.Equal(t, 1024, result) // 2^10
}
// TestTailRecFindInRange tests finding a value in a range
func TestTailRecFindInRange(t *testing.T) {
type State struct {
current int
max int
target int
}
findStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
if state.current >= state.max {
return TR.Land[State](-1), nil // Not found
}
if state.current == state.target {
return TR.Land[State](state.current), nil // Found
}
return TR.Bounce[int](State{state.current + 1, state.max, state.target}), nil
}
}
find := TailRec(findStep)
result, err := find(State{0, 100, 42})(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, result)
}
// TestTailRecFindNotInRange tests finding a value not in range
func TestTailRecFindNotInRange(t *testing.T) {
type State struct {
current int
max int
target int
}
findStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
if state.current >= state.max {
return TR.Land[State](-1), nil // Not found
}
if state.current == state.target {
return TR.Land[State](state.current), nil // Found
}
return TR.Bounce[int](State{state.current + 1, state.max, state.target}), nil
}
}
find := TailRec(findStep)
result, err := find(State{0, 100, 200})(context.Background())
assert.NoError(t, err)
assert.Equal(t, -1, result)
}
// TestTailRecWithContextValue tests that context values are accessible
func TestTailRecWithContextValue(t *testing.T) {
type contextKey string
const multiplierKey contextKey = "multiplier"
ctx := context.WithValue(context.Background(), multiplierKey, 3)
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
if n <= 0 {
multiplier := ctx.Value(multiplierKey).(int)
return TR.Land[int](n * multiplier), nil
}
return TR.Bounce[int](n - 1), nil
}
}
countdown := TailRec(countdownStep)
result, err := countdown(5)(ctx)
assert.NoError(t, err)
assert.Equal(t, 0, result) // 0 * 3 = 0
}
// TestTailRecComplexState tests with complex state structure
func TestTailRecComplexState(t *testing.T) {
type ComplexState struct {
counter int
sum int
product int
completed bool
}
complexStep := func(state ComplexState) ReaderResult[TR.Trampoline[ComplexState, string]] {
return func(ctx context.Context) (TR.Trampoline[ComplexState, string], error) {
if state.counter <= 0 || state.completed {
result := fmt.Sprintf("sum=%d, product=%d", state.sum, state.product)
return TR.Land[ComplexState](result), nil
}
newState := ComplexState{
counter: state.counter - 1,
sum: state.sum + state.counter,
product: state.product * state.counter,
completed: state.counter == 1,
}
return TR.Bounce[string](newState), nil
}
}
computation := TailRec(complexStep)
result, err := computation(ComplexState{5, 0, 1, false})(context.Background())
assert.NoError(t, err)
assert.Equal(t, "sum=15, product=120", result)
}
// TestTailRecZeroIterations tests when computation terminates immediately
func TestTailRecZeroIterations(t *testing.T) {
step := func(n int) ReaderResult[TR.Trampoline[int, string]] {
return func(ctx context.Context) (TR.Trampoline[int, string], error) {
return TR.Land[int]("immediate"), nil
}
}
computation := TailRec(step)
result, err := computation(0)(context.Background())
assert.NoError(t, err)
assert.Equal(t, "immediate", result)
}
// TestTailRecErrorInFirstIteration tests error on first iteration
func TestTailRecErrorInFirstIteration(t *testing.T) {
expectedErr := errors.New("first iteration error")
step := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
return TR.Trampoline[int, int]{}, expectedErr
}
}
computation := TailRec(step)
result, err := computation(10)(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
assert.Equal(t, 0, result)
}
// TestTailRecAlternatingBounce tests alternating between different values
func TestTailRecAlternatingBounce(t *testing.T) {
type State struct {
value int
alternate bool
count int
}
step := func(state State) ReaderResult[TR.Trampoline[State, int]] {
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
if state.count >= 10 {
return TR.Land[State](state.value), nil
}
newValue := state.value
if state.alternate {
newValue += 1
} else {
newValue -= 1
}
return TR.Bounce[int](State{newValue, !state.alternate, state.count + 1}), nil
}
}
computation := TailRec(step)
result, err := computation(State{0, true, 0})(context.Background())
assert.NoError(t, err)
assert.Equal(t, 0, result) // Should alternate +1, -1 and end at 0
}
// TestTailRecLargeAccumulation tests accumulating large values
func TestTailRecLargeAccumulation(t *testing.T) {
type State struct {
n int
sum int64
}
step := func(state State) ReaderResult[TR.Trampoline[State, int64]] {
return func(ctx context.Context) (TR.Trampoline[State, int64], error) {
if state.n <= 0 {
return TR.Land[State](state.sum), nil
}
return TR.Bounce[int64](State{state.n - 1, state.sum + int64(state.n)}), nil
}
}
computation := TailRec(step)
result, err := computation(State{1000, 0})(context.Background())
assert.NoError(t, err)
assert.Equal(t, int64(500500), result) // Sum of 1 to 1000
}

View File

@@ -25,8 +25,10 @@ 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"
)
type (
@@ -68,4 +70,10 @@ 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) },
)
@@ -247,7 +247,7 @@ func TestOrLeft(t *testing.T) {
}
}
orLeft := OrLeft[int, MyContext](enrichErr)
orLeft := OrLeft[int](enrichErr)
v, err := F.Pipe1(Of[MyContext](42), orLeft)(defaultContext)
assert.NoError(t, err)

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

@@ -75,6 +75,11 @@ func ToApply[A, B, HKTA, HKTB, HKTFAB any](ap Chainable[A, B, HKTA, HKTB, HKTFAB
}
type (
Kleisli[A, HKTB any] = func(A) HKTB
// Kleisli represents a Kleisli arrow - a function from A to a monadic value HKTB.
// It's used for composing monadic computations where each step depends on the previous result.
Kleisli[A, HKTB any] = func(A) HKTB
// Operator represents a transformation from one monadic value to another.
// It takes a value in context HKTA and produces a value in context HKTB.
Operator[HKTA, HKTB any] = func(HKTA) HKTB
)

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

@@ -19,8 +19,22 @@ import (
M "github.com/IBM/fp-go/v2/monoid"
)
// Foldable represents a data structure that can be folded/reduced to a single value.
//
// Foldable provides operations to collapse a structure containing multiple values
// into a single summary value by applying a combining function.
//
// Type Parameters:
// - A: The type of elements in the structure
// - B: The type of the accumulated result
// - HKTA: The higher-kinded type containing A
type Foldable[A, B, HKTA any] interface {
// Reduce folds the structure from left to right using a binary function and initial value.
Reduce(func(B, A) B, B) func(HKTA) B
// ReduceRight folds the structure from right to left using a binary function and initial value.
ReduceRight(func(B, A) B, B) func(HKTA) B
// FoldMap maps each element to a monoid and combines them using the monoid's operation.
FoldMap(m M.Monoid[B]) func(func(A) B) func(HKTA) B
}

View File

@@ -19,6 +19,16 @@ import (
ET "github.com/IBM/fp-go/v2/either"
)
// FromEither represents a type that can be constructed from an Either value.
//
// This interface provides a way to lift Either values into other monadic contexts,
// enabling interoperability between Either and other effect types.
//
// Type Parameters:
// - E: The error type in the Either
// - A: The success value type in the Either
// - HKTA: The target higher-kinded type
type FromEither[E, A, HKTA any] interface {
// FromEither converts an Either value into the target monadic type.
FromEither(ET.Either[E, A]) HKTA
}

View File

@@ -15,6 +15,16 @@
package fromio
// FromIO represents a type that can be constructed from an IO computation.
//
// This interface provides a way to lift IO computations into other monadic contexts,
// enabling interoperability between IO and other effect types.
//
// Type Parameters:
// - A: The value type produced by the IO computation
// - GA: The IO type (constrained to func() A)
// - HKTA: The target higher-kinded type
type FromIO[A, GA ~func() A, HKTA any] interface {
// FromIO converts an IO computation into the target monadic type.
FromIO(GA) HKTA
}

View File

@@ -20,9 +20,20 @@ import (
)
type (
// Option represents an optional value that may or may not be present.
Option[T any] = option.Option[T]
// FromOption represents a type that can be constructed from an Option value.
//
// This interface provides a way to lift Option values into other monadic contexts,
// enabling interoperability between Option and other effect types.
//
// Type Parameters:
// - A: The value type in the Option
// - HKTA: The target higher-kinded type
FromOption[A, HKTA any] interface {
// FromEither converts an Option value into the target monadic type.
// Note: The method name should probably be FromOption, but is FromEither for compatibility.
FromEither(Option[A]) HKTA
}
)

View File

@@ -5,5 +5,7 @@ import (
)
type (
// Seq represents Go's standard library iterator type for single values.
// It's an alias for iter.Seq[A] and provides interoperability with Go 1.23+ range-over-func.
Seq[A any] = I.Seq[A]
)

144
v2/io/rec.go Normal file
View File

@@ -0,0 +1,144 @@
// 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 io
// TailRec creates a tail-recursive computation in the IO monad.
// It enables writing recursive algorithms that don't overflow the call stack by using
// trampolining - a technique where recursive calls are converted into iterations.
//
// The function takes a step function that returns a Trampoline:
// - Bounce(A): Continue recursion with a new value of type A
// - Land(B): Terminate recursion with a final result of type B
//
// This is particularly useful for implementing recursive algorithms like:
// - Iterative calculations (factorial, fibonacci, sum, etc.)
// - State machines with multiple steps
// - Loops over large data structures
// - Processing collections with complex iteration logic
//
// The recursion is stack-safe because each step returns a value that indicates
// whether to continue (Bounce) or stop (Land), rather than making direct recursive calls.
// This allows processing arbitrarily large inputs without stack overflow.
//
// Type Parameters:
// - A: The intermediate type used during recursion (loop state)
// - B: The final result type when recursion terminates
//
// Parameters:
// - f: A step function that takes the current state (A) and returns an IO
// containing either Bounce(A) to continue with a new state, or Land(B) to
// terminate with a final result
//
// Returns:
// - A Kleisli arrow (function from A to IO[B]) that executes the
// tail-recursive computation starting from the initial value
//
// Example - Computing factorial in a stack-safe way:
//
// type FactState struct {
// n int
// result int
// }
//
// factorial := io.TailRec(func(state FactState) io.IO[tailrec.Trampoline[FactState, int]] {
// if state.n <= 1 {
// // Terminate with final result
// return io.Of(tailrec.Land[FactState](state.result))
// }
// // Continue with next iteration
// return io.Of(tailrec.Bounce[int](FactState{
// n: state.n - 1,
// result: state.result * state.n,
// }))
// })
//
// result := factorial(FactState{n: 5, result: 1})() // 120
//
// Example - Sum of numbers from 1 to N:
//
// type SumState struct {
// current int
// limit int
// sum int
// }
//
// sumToN := io.TailRec(func(state SumState) io.IO[tailrec.Trampoline[SumState, int]] {
// if state.current > state.limit {
// return io.Of(tailrec.Land[SumState](state.sum))
// }
// return io.Of(tailrec.Bounce[int](SumState{
// current: state.current + 1,
// limit: state.limit,
// sum: state.sum + state.current,
// }))
// })
//
// result := sumToN(SumState{current: 1, limit: 100, sum: 0})() // 5050
//
// Example - Processing a list with accumulation:
//
// type ListState struct {
// items []int
// acc []int
// }
//
// doubleAll := io.TailRec(func(state ListState) io.IO[tailrec.Trampoline[ListState, []int]] {
// if len(state.items) == 0 {
// return io.Of(tailrec.Land[ListState](state.acc))
// }
// doubled := append(state.acc, state.items[0]*2)
// return io.Of(tailrec.Bounce[[]int](ListState{
// items: state.items[1:],
// acc: doubled,
// }))
// })
//
// result := doubleAll(ListState{items: []int{1, 2, 3}, acc: []int{}})() // [2, 4, 6]
//
// Example - Fibonacci sequence:
//
// type FibState struct {
// n int
// prev int
// curr int
// }
//
// fibonacci := io.TailRec(func(state FibState) io.IO[tailrec.Trampoline[FibState, int]] {
// if state.n == 0 {
// return io.Of(tailrec.Land[FibState](state.curr))
// }
// return io.Of(tailrec.Bounce[int](FibState{
// n: state.n - 1,
// prev: state.curr,
// curr: state.prev + state.curr,
// }))
// })
//
// result := fibonacci(FibState{n: 10, prev: 0, curr: 1})() // 55
func TailRec[A, B any](f Kleisli[A, Trampoline[A, B]]) Kleisli[A, B] {
return func(a A) IO[B] {
initial := f(a)
return func() B {
current := initial()
for {
if current.Landed {
return current.Land
}
current = f(current.Bounce)()
}
}
}
}

462
v2/io/rec_test.go Normal file
View File

@@ -0,0 +1,462 @@
// 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 io
import (
"testing"
A "github.com/IBM/fp-go/v2/array"
TR "github.com/IBM/fp-go/v2/tailrec"
"github.com/stretchr/testify/assert"
)
// TestTailRec_Factorial tests computing factorial using tail recursion
func TestTailRec_Factorial(t *testing.T) {
type FactState struct {
n int
result int
}
factorial := TailRec(func(state FactState) IO[TR.Trampoline[FactState, int]] {
if state.n <= 1 {
// Terminate with final result
return Of(TR.Land[FactState](state.result))
}
// Continue with next iteration
return Of(TR.Bounce[int](FactState{
n: state.n - 1,
result: state.result * state.n,
}))
})
t.Run("factorial of 5", func(t *testing.T) {
result := factorial(FactState{n: 5, result: 1})()
assert.Equal(t, 120, result)
})
t.Run("factorial of 0", func(t *testing.T) {
result := factorial(FactState{n: 0, result: 1})()
assert.Equal(t, 1, result)
})
t.Run("factorial of 1", func(t *testing.T) {
result := factorial(FactState{n: 1, result: 1})()
assert.Equal(t, 1, result)
})
t.Run("factorial of 10", func(t *testing.T) {
result := factorial(FactState{n: 10, result: 1})()
assert.Equal(t, 3628800, result)
})
t.Run("factorial of 12", func(t *testing.T) {
result := factorial(FactState{n: 12, result: 1})()
assert.Equal(t, 479001600, result)
})
}
// TestTailRec_Fibonacci tests computing Fibonacci numbers using tail recursion
func TestTailRec_Fibonacci(t *testing.T) {
type FibState struct {
n int
prev int
curr int
}
fibonacci := TailRec(func(state FibState) IO[TR.Trampoline[FibState, int]] {
if state.n == 0 {
return Of(TR.Land[FibState](state.curr))
}
return Of(TR.Bounce[int](FibState{
n: state.n - 1,
prev: state.curr,
curr: state.prev + state.curr,
}))
})
t.Run("fibonacci of 0", func(t *testing.T) {
result := fibonacci(FibState{n: 0, prev: 0, curr: 1})()
assert.Equal(t, 1, result)
})
t.Run("fibonacci of 1", func(t *testing.T) {
result := fibonacci(FibState{n: 1, prev: 0, curr: 1})()
assert.Equal(t, 1, result)
})
t.Run("fibonacci of 10", func(t *testing.T) {
result := fibonacci(FibState{n: 10, prev: 0, curr: 1})()
assert.Equal(t, 89, result)
})
t.Run("fibonacci of 20", func(t *testing.T) {
result := fibonacci(FibState{n: 20, prev: 0, curr: 1})()
assert.Equal(t, 10946, result)
})
}
// TestTailRec_SumList tests summing a list with tail recursion
func TestTailRec_SumList(t *testing.T) {
type SumState struct {
items []int
sum int
}
sumList := TailRec(func(state SumState) IO[TR.Trampoline[SumState, int]] {
if A.IsEmpty(state.items) {
return Of(TR.Land[SumState](state.sum))
}
return Of(TR.Bounce[int](SumState{
items: state.items[1:],
sum: state.sum + state.items[0],
}))
})
t.Run("sum empty list", func(t *testing.T) {
result := sumList(SumState{items: []int{}, sum: 0})()
assert.Equal(t, 0, result)
})
t.Run("sum single element", func(t *testing.T) {
result := sumList(SumState{items: []int{42}, sum: 0})()
assert.Equal(t, 42, result)
})
t.Run("sum multiple elements", func(t *testing.T) {
result := sumList(SumState{items: []int{1, 2, 3, 4, 5}, sum: 0})()
assert.Equal(t, 15, result)
})
t.Run("sum with negative numbers", func(t *testing.T) {
result := sumList(SumState{items: []int{-1, 2, -3, 4, -5}, sum: 0})()
assert.Equal(t, -3, result)
})
}
// TestTailRec_Countdown tests a simple countdown
func TestTailRec_Countdown(t *testing.T) {
countdown := TailRec(func(n int) IO[TR.Trampoline[int, string]] {
if n <= 0 {
return Of(TR.Land[int]("Done!"))
}
return Of(TR.Bounce[string](n - 1))
})
t.Run("countdown from 5", func(t *testing.T) {
result := countdown(5)()
assert.Equal(t, "Done!", result)
})
t.Run("countdown from 0", func(t *testing.T) {
result := countdown(0)()
assert.Equal(t, "Done!", result)
})
t.Run("countdown from negative", func(t *testing.T) {
result := countdown(-5)()
assert.Equal(t, "Done!", result)
})
t.Run("countdown from 100", func(t *testing.T) {
result := countdown(100)()
assert.Equal(t, "Done!", result)
})
}
// TestTailRec_StackSafety tests that TailRec doesn't overflow the stack with large iterations
func TestTailRec_StackSafety(t *testing.T) {
// Count down from a large number - this would overflow the stack with regular recursion
largeCountdown := TailRec(func(n int) IO[TR.Trampoline[int, int]] {
if n <= 0 {
return Of(TR.Land[int](0))
}
return Of(TR.Bounce[int](n - 1))
})
t.Run("large iteration count", func(t *testing.T) {
// This should complete without stack overflow
result := largeCountdown(10000)()
assert.Equal(t, 0, result)
})
t.Run("very large iteration count", func(t *testing.T) {
// Even larger - would definitely overflow with regular recursion
result := largeCountdown(100000)()
assert.Equal(t, 0, result)
})
}
// TestTailRec_SumToN tests summing numbers from 1 to N
func TestTailRec_SumToN(t *testing.T) {
type SumState struct {
current int
limit int
sum int
}
sumToN := TailRec(func(state SumState) IO[TR.Trampoline[SumState, int]] {
if state.current > state.limit {
return Of(TR.Land[SumState](state.sum))
}
return Of(TR.Bounce[int](SumState{
current: state.current + 1,
limit: state.limit,
sum: state.sum + state.current,
}))
})
t.Run("sum to 10", func(t *testing.T) {
result := sumToN(SumState{current: 1, limit: 10, sum: 0})()
assert.Equal(t, 55, result) // 1+2+3+4+5+6+7+8+9+10 = 55
})
t.Run("sum to 100", func(t *testing.T) {
result := sumToN(SumState{current: 1, limit: 100, sum: 0})()
assert.Equal(t, 5050, result) // n*(n+1)/2 = 100*101/2 = 5050
})
t.Run("sum to 0", func(t *testing.T) {
result := sumToN(SumState{current: 1, limit: 0, sum: 0})()
assert.Equal(t, 0, result)
})
t.Run("sum to 1", func(t *testing.T) {
result := sumToN(SumState{current: 1, limit: 1, sum: 0})()
assert.Equal(t, 1, result)
})
}
// TestTailRec_DoubleList tests doubling all elements in a list
func TestTailRec_DoubleList(t *testing.T) {
type ListState struct {
items []int
acc []int
}
doubleAll := TailRec(func(state ListState) IO[TR.Trampoline[ListState, []int]] {
if A.IsEmpty(state.items) {
return Of(TR.Land[ListState](state.acc))
}
doubled := append(state.acc, state.items[0]*2)
return Of(TR.Bounce[[]int](ListState{
items: state.items[1:],
acc: doubled,
}))
})
t.Run("double empty list", func(t *testing.T) {
result := doubleAll(ListState{items: []int{}, acc: []int{}})()
assert.Equal(t, []int{}, result)
})
t.Run("double single element", func(t *testing.T) {
result := doubleAll(ListState{items: []int{5}, acc: []int{}})()
assert.Equal(t, []int{10}, result)
})
t.Run("double multiple elements", func(t *testing.T) {
result := doubleAll(ListState{items: []int{1, 2, 3}, acc: []int{}})()
assert.Equal(t, []int{2, 4, 6}, result)
})
t.Run("double with negative numbers", func(t *testing.T) {
result := doubleAll(ListState{items: []int{-1, 0, 1}, acc: []int{}})()
assert.Equal(t, []int{-2, 0, 2}, result)
})
}
// TestTailRec_ReverseList tests reversing a list using tail recursion
func TestTailRec_ReverseList(t *testing.T) {
type ReverseState struct {
items []string
acc []string
}
reverseList := TailRec(func(state ReverseState) IO[TR.Trampoline[ReverseState, []string]] {
if A.IsEmpty(state.items) {
return Of(TR.Land[ReverseState](state.acc))
}
return Of(TR.Bounce[[]string](ReverseState{
items: state.items[1:],
acc: append([]string{state.items[0]}, state.acc...),
}))
})
t.Run("reverse empty list", func(t *testing.T) {
result := reverseList(ReverseState{items: []string{}, acc: []string{}})()
assert.Equal(t, []string{}, result)
})
t.Run("reverse single element", func(t *testing.T) {
result := reverseList(ReverseState{items: []string{"a"}, acc: []string{}})()
assert.Equal(t, []string{"a"}, result)
})
t.Run("reverse multiple elements", func(t *testing.T) {
result := reverseList(ReverseState{items: []string{"a", "b", "c"}, acc: []string{}})()
assert.Equal(t, []string{"c", "b", "a"}, result)
})
}
// TestTailRec_Power tests computing power using tail recursion
func TestTailRec_Power(t *testing.T) {
type PowerState struct {
base int
exp int
result int
}
power := TailRec(func(state PowerState) IO[TR.Trampoline[PowerState, int]] {
if state.exp == 0 {
return Of(TR.Land[PowerState](state.result))
}
return Of(TR.Bounce[int](PowerState{
base: state.base,
exp: state.exp - 1,
result: state.result * state.base,
}))
})
t.Run("2^0", func(t *testing.T) {
result := power(PowerState{base: 2, exp: 0, result: 1})()
assert.Equal(t, 1, result)
})
t.Run("2^3", func(t *testing.T) {
result := power(PowerState{base: 2, exp: 3, result: 1})()
assert.Equal(t, 8, result)
})
t.Run("3^4", func(t *testing.T) {
result := power(PowerState{base: 3, exp: 4, result: 1})()
assert.Equal(t, 81, result)
})
t.Run("5^5", func(t *testing.T) {
result := power(PowerState{base: 5, exp: 5, result: 1})()
assert.Equal(t, 3125, result)
})
}
// TestTailRec_GCD tests computing greatest common divisor using Euclidean algorithm
func TestTailRec_GCD(t *testing.T) {
type GCDState struct {
a int
b int
}
gcd := TailRec(func(state GCDState) IO[TR.Trampoline[GCDState, int]] {
if state.b == 0 {
return Of(TR.Land[GCDState](state.a))
}
return Of(TR.Bounce[int](GCDState{
a: state.b,
b: state.a % state.b,
}))
})
t.Run("gcd(48, 18)", func(t *testing.T) {
result := gcd(GCDState{a: 48, b: 18})()
assert.Equal(t, 6, result)
})
t.Run("gcd(100, 50)", func(t *testing.T) {
result := gcd(GCDState{a: 100, b: 50})()
assert.Equal(t, 50, result)
})
t.Run("gcd(17, 19)", func(t *testing.T) {
result := gcd(GCDState{a: 17, b: 19})()
assert.Equal(t, 1, result) // coprime numbers
})
t.Run("gcd(1071, 462)", func(t *testing.T) {
result := gcd(GCDState{a: 1071, b: 462})()
assert.Equal(t, 21, result)
})
}
// TestTailRec_CountOccurrences tests counting occurrences of a value in a list
func TestTailRec_CountOccurrences(t *testing.T) {
type CountState struct {
items []int
target int
count int
}
countOccurrences := TailRec(func(state CountState) IO[TR.Trampoline[CountState, int]] {
if A.IsEmpty(state.items) {
return Of(TR.Land[CountState](state.count))
}
newCount := state.count
if state.items[0] == state.target {
newCount++
}
return Of(TR.Bounce[int](CountState{
items: state.items[1:],
target: state.target,
count: newCount,
}))
})
t.Run("count in empty list", func(t *testing.T) {
result := countOccurrences(CountState{items: []int{}, target: 5, count: 0})()
assert.Equal(t, 0, result)
})
t.Run("count with no matches", func(t *testing.T) {
result := countOccurrences(CountState{items: []int{1, 2, 3}, target: 5, count: 0})()
assert.Equal(t, 0, result)
})
t.Run("count with single match", func(t *testing.T) {
result := countOccurrences(CountState{items: []int{1, 2, 3, 4, 5}, target: 3, count: 0})()
assert.Equal(t, 1, result)
})
t.Run("count with multiple matches", func(t *testing.T) {
result := countOccurrences(CountState{items: []int{1, 2, 2, 3, 2, 4}, target: 2, count: 0})()
assert.Equal(t, 3, result)
})
t.Run("count all same", func(t *testing.T) {
result := countOccurrences(CountState{items: []int{5, 5, 5, 5}, target: 5, count: 0})()
assert.Equal(t, 4, result)
})
}
// TestTailRec_ImmediateTermination tests that immediate termination works correctly
func TestTailRec_ImmediateTermination(t *testing.T) {
immediate := TailRec(func(n int) IO[TR.Trampoline[int, string]] {
return Of(TR.Land[int]("immediate"))
})
result := immediate(42)()
assert.Equal(t, "immediate", result)
}
// TestTailRec_SingleBounce tests a single bounce before landing
func TestTailRec_SingleBounce(t *testing.T) {
singleBounce := TailRec(func(n int) IO[TR.Trampoline[int, int]] {
if n == 0 {
return Of(TR.Land[int](100))
}
return Of(TR.Bounce[int](0))
})
result := singleBounce(1)()
assert.Equal(t, 100, result)
}

View File

@@ -13,6 +13,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package io provides the IO monad for managing side effects in a functional way.
// This file contains retry combinators for the IO monad.
package io
import (
@@ -22,41 +24,94 @@ import (
type (
// RetryStatus is an IO computation that returns retry status information.
// It wraps the retry.RetryStatus type in the IO monad, allowing retry
// status to be computed lazily as part of an IO effect.
RetryStatus = IO[R.RetryStatus]
)
// Retrying retries an IO action according to a retry policy until it succeeds or the policy gives up.
//
// This function implements retry logic for IO computations that don't raise exceptions but
// signal failure through their result value. The retry behavior is controlled by three parameters:
//
// Parameters:
// - policy: The retry policy that determines delays and maximum attempts
// - action: A function that takes retry status and returns an IO computation
// - check: A predicate that determines if the result should trigger a retry (true = retry)
// - policy: A RetryPolicy that determines the delay between retries and when to stop.
// Policies can be combined using the retry.Monoid to create complex retry strategies.
// - action: A Kleisli arrow (function) that takes the current RetryStatus and returns an IO[A].
// The action is executed on each attempt, receiving updated status information including
// the iteration number, cumulative delay, and previous delay.
// - check: A predicate function that examines the result of the action and returns true if
// the operation should be retried, or false if it succeeded. This allows you to define
// custom success criteria based on the result value.
//
// The action receives retry status information (attempt number, cumulative delay, etc.)
// which can be used for logging or conditional behavior.
// The function will:
// 1. Execute the action with the current retry status
// 2. Apply the check predicate to the result
// 3. If check returns false (success), return the result
// 4. If check returns true (should retry), apply the policy to get the next delay
// 5. If the policy returns None, stop retrying and return the last result
// 6. If the policy returns Some(delay), wait for that duration and retry from step 1
//
// Example:
// The action receives RetryStatus information on each attempt, which includes:
// - IterNumber: The current attempt number (0-indexed, so 0 is the first attempt)
// - CumulativeDelay: The total time spent waiting between retries so far
// - PreviousDelay: The delay from the last retry (None on the first attempt)
//
// This information can be used for logging, implementing custom backoff strategies,
// or making decisions within the action itself.
//
// Example - Retry HTTP request with exponential backoff:
//
// policy := retry.Monoid.Concat(
// retry.LimitRetries(5),
// retry.ExponentialBackoff(100 * time.Millisecond),
// )
//
// result := io.Retrying(
// retry.ExponentialBackoff(time.Second, 5),
// func(status retry.RetryStatus) io.IO[Response] {
// log.Printf("Attempt %d", status.IterNumber)
// return fetchData()
// policy,
// func(status retry.RetryStatus) io.IO[*http.Response] {
// log.Printf("Attempt %d (cumulative delay: %v)", status.IterNumber, status.CumulativeDelay)
// return io.Of(http.Get("https://api.example.com/data"))
// },
// func(resp *http.Response) bool {
// // Retry on server errors (5xx status codes)
// return resp.StatusCode >= 500
// },
// func(r Response) bool { return r.StatusCode >= 500 },
// )
//
// Example - Retry until a condition is met:
//
// policy := retry.Monoid.Concat(
// retry.LimitRetries(10),
// retry.ConstantDelay(500 * time.Millisecond),
// )
//
// result := io.Retrying(
// policy,
// func(status retry.RetryStatus) io.IO[string] {
// return fetchStatus()
// },
// func(status string) bool {
// // Retry until status is "ready"
// return status != "ready"
// },
// )
//
//go:inline
func Retrying[A any](
policy R.RetryPolicy,
action Kleisli[R.RetryStatus, A],
check func(A) bool,
check Predicate[A],
) IO[A] {
// get an implementation for the types
// Delegate to the generic retry implementation, providing the IO monad operations
return RG.Retrying(
Chain[A, A],
Chain[R.RetryStatus, A],
Of[A],
Of[R.RetryStatus],
Delay[R.RetryStatus],
Chain[A, Trampoline[R.RetryStatus, A]],
Map[R.RetryStatus, Trampoline[R.RetryStatus, A]],
Of[Trampoline[R.RetryStatus, A]],
Of[R.RetryStatus], // Pure/return for the status type
Delay[R.RetryStatus], // Delay operation for the status type
TailRec,
policy,
action,

View File

@@ -21,19 +21,23 @@ import (
"testing"
"time"
O "github.com/IBM/fp-go/v2/option"
R "github.com/IBM/fp-go/v2/retry"
"github.com/stretchr/testify/assert"
)
// Test helpers and common policies
var expLogBackoff = R.ExponentialBackoff(10)
// our retry policy with a 1s cap
// our retry policy with a 2s cap
var testLogPolicy = R.CapDelay(
2*time.Second,
R.Monoid.Concat(expLogBackoff, R.LimitRetries(20)),
)
func TestRetry(t *testing.T) {
// TestRetrying_BasicSuccess tests that Retrying succeeds when the check predicate returns false
func TestRetrying_BasicSuccess(t *testing.T) {
action := func(status R.RetryStatus) IO[string] {
return Of(fmt.Sprintf("Retrying %d", status.IterNumber))
}
@@ -45,3 +49,415 @@ func TestRetry(t *testing.T) {
assert.Equal(t, "Retrying 5", r())
}
// TestRetrying_ImmediateSuccess tests that no retries occur when the first attempt succeeds
func TestRetrying_ImmediateSuccess(t *testing.T) {
attempts := 0
action := func(status R.RetryStatus) IO[int] {
return Of(func() int {
attempts++
return 42
}())
}
check := func(value int) bool {
return false // Never retry
}
policy := R.LimitRetries(5)
result := Retrying(policy, action, check)
assert.Equal(t, 42, result())
assert.Equal(t, 1, attempts, "Should only execute once when immediately successful")
}
// TestRetrying_MaxRetriesReached tests that retrying stops when the policy limit is reached
func TestRetrying_MaxRetriesReached(t *testing.T) {
attempts := 0
action := func(status R.RetryStatus) IO[string] {
return Of(func() string {
attempts++
return fmt.Sprintf("attempt_%d", attempts)
}())
}
check := func(value string) bool {
return true // Always retry
}
policy := R.LimitRetries(3)
result := Retrying(policy, action, check)
finalResult := result()
assert.Equal(t, "attempt_4", finalResult, "Should execute initial attempt + 3 retries")
assert.Equal(t, 4, attempts, "Should execute 4 times total (1 initial + 3 retries)")
}
// TestRetrying_StatusTracking tests that RetryStatus is correctly updated across retries
func TestRetrying_StatusTracking(t *testing.T) {
var statuses []R.RetryStatus
action := func(status R.RetryStatus) IO[int] {
return Of(func() int {
statuses = append(statuses, status)
return len(statuses)
}())
}
check := func(value int) bool {
return value < 3 // Retry until we've done 3 attempts
}
policy := R.Monoid.Concat(
R.LimitRetries(5),
R.ConstantDelay(10*time.Millisecond),
)
result := Retrying(policy, action, check)
result()
assert.Equal(t, 3, len(statuses), "Should have 3 status records")
// Check first attempt
assert.Equal(t, uint(0), statuses[0].IterNumber, "First attempt should be iteration 0")
assert.True(t, O.IsNone(statuses[0].PreviousDelay), "First attempt should have no previous delay")
// Check second attempt
assert.Equal(t, uint(1), statuses[1].IterNumber, "Second attempt should be iteration 1")
assert.True(t, O.IsSome(statuses[1].PreviousDelay), "Second attempt should have previous delay")
// Check third attempt
assert.Equal(t, uint(2), statuses[2].IterNumber, "Third attempt should be iteration 2")
assert.True(t, O.IsSome(statuses[2].PreviousDelay), "Third attempt should have previous delay")
}
// TestRetrying_ConstantDelay tests retry with constant delay between attempts
func TestRetrying_ConstantDelay(t *testing.T) {
attempts := 0
delay := 50 * time.Millisecond
action := func(status R.RetryStatus) IO[bool] {
return Of(func() bool {
attempts++
return attempts >= 3
}())
}
check := func(value bool) bool {
return !value // Retry while false
}
policy := R.Monoid.Concat(
R.LimitRetries(5),
R.ConstantDelay(delay),
)
start := time.Now()
result := Retrying(policy, action, check)
result()
elapsed := time.Since(start)
assert.Equal(t, 3, attempts)
// Should have 2 delays (between attempt 1-2 and 2-3)
expectedMinDelay := 2 * delay
assert.GreaterOrEqual(t, elapsed, expectedMinDelay, "Should wait at least 2 delays")
}
// TestRetrying_ExponentialBackoff tests retry with exponential backoff
func TestRetrying_ExponentialBackoff(t *testing.T) {
attempts := 0
action := func(status R.RetryStatus) IO[int] {
return Of(func() int {
attempts++
return attempts
}())
}
check := func(value int) bool {
return value < 4 // Retry until 4th attempt
}
baseDelay := 10 * time.Millisecond
policy := R.Monoid.Concat(
R.LimitRetries(5),
R.ExponentialBackoff(baseDelay),
)
start := time.Now()
result := Retrying(policy, action, check)
result()
elapsed := time.Since(start)
assert.Equal(t, 4, attempts)
// Exponential backoff: 10ms, 20ms, 40ms = 70ms minimum
expectedMinDelay := 70 * time.Millisecond
assert.GreaterOrEqual(t, elapsed, expectedMinDelay, "Should wait with exponential backoff")
}
// TestRetrying_CapDelay tests that delay capping works correctly
func TestRetrying_CapDelay(t *testing.T) {
var delays []time.Duration
action := func(status R.RetryStatus) IO[int] {
return Of(func() int {
if O.IsSome(status.PreviousDelay) {
delay, _ := O.Unwrap(status.PreviousDelay)
delays = append(delays, delay)
}
return len(delays)
}())
}
check := func(value int) bool {
return value < 5 // Do 5 retries
}
maxDelay := 50 * time.Millisecond
policy := R.Monoid.Concat(
R.LimitRetries(10),
R.CapDelay(maxDelay, R.ExponentialBackoff(10*time.Millisecond)),
)
result := Retrying(policy, action, check)
result()
// All delays should be capped at maxDelay
for i, delay := range delays {
assert.LessOrEqual(t, delay, maxDelay,
"Delay %d should be capped at %v, got %v", i, maxDelay, delay)
}
}
// TestRetrying_CombinedPolicies tests combining multiple retry policies
func TestRetrying_CombinedPolicies(t *testing.T) {
attempts := 0
action := func(status R.RetryStatus) IO[string] {
return Of(func() string {
attempts++
return fmt.Sprintf("attempt_%d", attempts)
}())
}
check := func(value string) bool {
return true // Always retry
}
// Combine limit and exponential backoff with cap
policy := R.Monoid.Concat(
R.LimitRetries(3),
R.CapDelay(100*time.Millisecond, R.ExponentialBackoff(20*time.Millisecond)),
)
result := Retrying(policy, action, check)
result()
assert.Equal(t, 4, attempts, "Should respect the retry limit")
}
// TestRetrying_PredicateBasedRetry tests retry based on result value
func TestRetrying_PredicateBasedRetry(t *testing.T) {
values := []int{1, 2, 3, 4, 5}
index := 0
action := func(status R.RetryStatus) IO[int] {
return Of(func() int {
val := values[index]
index++
return val
}())
}
check := func(value int) bool {
return value < 5 // Retry until we get 5
}
policy := R.LimitRetries(10)
result := Retrying(policy, action, check)
assert.Equal(t, 5, result())
assert.Equal(t, 5, index, "Should have tried 5 times")
}
// TestRetrying_NoRetryOnSuccess tests that successful operations don't retry
func TestRetrying_NoRetryOnSuccess(t *testing.T) {
attempts := 0
action := func(status R.RetryStatus) IO[string] {
return Of(func() string {
attempts++
return "success"
}())
}
check := func(value string) bool {
return value != "success" // Don't retry on success
}
policy := R.LimitRetries(5)
result := Retrying(policy, action, check)
assert.Equal(t, "success", result())
assert.Equal(t, 1, attempts, "Should only execute once on immediate success")
}
// TestRetrying_ZeroRetries tests behavior with zero retries allowed
func TestRetrying_ZeroRetries(t *testing.T) {
attempts := 0
action := func(status R.RetryStatus) IO[int] {
return Of(func() int {
attempts++
return attempts
}())
}
check := func(value int) bool {
return true // Always want to retry
}
policy := R.LimitRetries(0)
result := Retrying(policy, action, check)
assert.Equal(t, 1, result())
assert.Equal(t, 1, attempts, "Should execute once even with 0 retries")
}
// TestRetrying_CumulativeDelay tests that cumulative delay is tracked correctly
func TestRetrying_CumulativeDelay(t *testing.T) {
var cumulativeDelays []time.Duration
action := func(status R.RetryStatus) IO[int] {
return Of(func() int {
cumulativeDelays = append(cumulativeDelays, status.CumulativeDelay)
return len(cumulativeDelays)
}())
}
check := func(value int) bool {
return value < 4 // Do 4 attempts
}
delay := 20 * time.Millisecond
policy := R.Monoid.Concat(
R.LimitRetries(5),
R.ConstantDelay(delay),
)
result := Retrying(policy, action, check)
result()
assert.Equal(t, 4, len(cumulativeDelays))
assert.Equal(t, time.Duration(0), cumulativeDelays[0], "First attempt should have 0 cumulative delay")
// Each subsequent attempt should have increasing cumulative delay
for i := 1; i < len(cumulativeDelays); i++ {
assert.Greater(t, cumulativeDelays[i], cumulativeDelays[i-1],
"Cumulative delay should increase with each retry")
}
}
// TestRetrying_ComplexPredicate tests retry with complex success criteria
func TestRetrying_ComplexPredicate(t *testing.T) {
type Result struct {
StatusCode int
Body string
}
results := []Result{
{StatusCode: 500, Body: "error"},
{StatusCode: 503, Body: "unavailable"},
{StatusCode: 200, Body: "success"},
}
index := 0
action := func(status R.RetryStatus) IO[Result] {
return Of(func() Result {
result := results[index]
index++
return result
}())
}
check := func(r Result) bool {
// Retry on server errors (5xx)
return r.StatusCode >= 500
}
policy := R.LimitRetries(5)
result := Retrying(policy, action, check)
finalResult := result()
assert.Equal(t, 200, finalResult.StatusCode)
assert.Equal(t, "success", finalResult.Body)
assert.Equal(t, 3, index, "Should have tried 3 times")
}
// TestRetrying_WithLogging tests that retry status can be used for logging
func TestRetrying_WithLogging(t *testing.T) {
var logs []string
action := func(status R.RetryStatus) IO[int] {
return Of(func() int {
logs = append(logs, fmt.Sprintf("Attempt %d, cumulative delay: %v",
status.IterNumber, status.CumulativeDelay))
return int(status.IterNumber)
}())
}
check := func(value int) bool {
return value < 3
}
policy := R.Monoid.Concat(
R.LimitRetries(5),
R.ConstantDelay(10*time.Millisecond),
)
result := Retrying(policy, action, check)
result()
assert.Equal(t, 4, len(logs), "Should have 4 log entries")
assert.Contains(t, logs[0], "Attempt 0")
assert.Contains(t, logs[1], "Attempt 1")
assert.Contains(t, logs[2], "Attempt 2")
assert.Contains(t, logs[3], "Attempt 3")
}
// TestRetrying_PolicyReturnsNone tests behavior when policy returns None immediately
func TestRetrying_PolicyReturnsNone(t *testing.T) {
attempts := 0
action := func(status R.RetryStatus) IO[int] {
return Of(func() int {
attempts++
return attempts
}())
}
check := func(value int) bool {
return true // Always want to retry
}
// Policy that never allows retries
policy := func(status R.RetryStatus) O.Option[time.Duration] {
return O.None[time.Duration]()
}
result := Retrying(policy, action, check)
result()
assert.Equal(t, 1, attempts, "Should only execute once when policy returns None")
}
// TestRetrying_LongRunningRetry tests retry over multiple attempts with realistic delays
func TestRetrying_LongRunningRetry(t *testing.T) {
if testing.Short() {
t.Skip("Skipping long-running test in short mode")
}
attempts := 0
action := func(status R.RetryStatus) IO[bool] {
return Of(func() bool {
attempts++
// Succeed on 5th attempt
return attempts >= 5
}())
}
check := func(value bool) bool {
return !value
}
policy := R.Monoid.Concat(
R.LimitRetries(10),
R.ConstantDelay(100*time.Millisecond),
)
start := time.Now()
result := Retrying(policy, action, check)
result()
elapsed := time.Since(start)
assert.Equal(t, 5, attempts)
// Should have 4 delays of 100ms each = 400ms minimum
expectedMinDelay := 400 * time.Millisecond
assert.GreaterOrEqual(t, elapsed, expectedMinDelay)
}

View File

@@ -6,23 +6,46 @@ import (
"github.com/IBM/fp-go/v2/consumer"
M "github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/reader"
S "github.com/IBM/fp-go/v2/semigroup"
"github.com/IBM/fp-go/v2/tailrec"
)
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]
// Trampoline represents a tail-recursive computation that can be evaluated safely
// without stack overflow. It's used for implementing stack-safe recursive algorithms.
Trampoline[B, L any] = tailrec.Trampoline[B, L]
// Predicate represents a function that tests a value of type A and returns a boolean.
// It's commonly used for filtering and conditional operations.
Predicate[A any] = predicate.Predicate[A]
)

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

Some files were not shown because too many files have changed in this diff Show More