mirror of
https://github.com/IBM/fp-go.git
synced 2026-01-13 00:44:11 +02:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a6b982779 | ||
|
|
9d31752887 | ||
|
|
14b52568b5 | ||
|
|
49227551b6 | ||
|
|
69691e9e70 | ||
|
|
d3c466bfb7 | ||
|
|
a6c6ea804f |
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ import (
|
||||
|
||||
// Create a pipeline of transformations
|
||||
pipeline := F.Flow3(
|
||||
A.Filter(func(x int) bool { return x > 0 }), // Keep positive numbers
|
||||
A.Filter(N.MoreThan(0)), // Keep positive numbers
|
||||
A.Map(N.Mul(2)), // Double each number
|
||||
A.Reduce(func(acc, x int) int { return acc + x }, 0), // Sum them up
|
||||
)
|
||||
|
||||
@@ -16,22 +16,11 @@
|
||||
package array
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/eq"
|
||||
)
|
||||
|
||||
func equals[T any](left, right []T, eq func(T, T) bool) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
for i, v1 := range left {
|
||||
v2 := right[i]
|
||||
if !eq(v1, v2) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Eq creates an equality checker for arrays given an equality checker for elements.
|
||||
// Two arrays are considered equal if they have the same length and all corresponding
|
||||
// elements are equal according to the provided Eq instance.
|
||||
@@ -46,6 +35,11 @@ func equals[T any](left, right []T, eq func(T, T) bool) bool {
|
||||
func Eq[T any](e E.Eq[T]) E.Eq[[]T] {
|
||||
eq := e.Equals
|
||||
return E.FromEquals(func(left, right []T) bool {
|
||||
return equals(left, right, eq)
|
||||
return slices.EqualFunc(left, right, eq)
|
||||
})
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func StrictEquals[T comparable]() E.Eq[[]T] {
|
||||
return E.FromEquals(slices.Equal[[]T])
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
// assert.ArrayNotEmpty(arr)(t)
|
||||
//
|
||||
// // Partial application - create reusable assertions
|
||||
// isPositive := assert.That(func(n int) bool { return n > 0 })
|
||||
// isPositive := assert.That(N.MoreThan(0))
|
||||
// // Later, apply to different values:
|
||||
// isPositive(42)(t) // Passes
|
||||
// isPositive(-5)(t) // Fails
|
||||
@@ -416,7 +416,7 @@ func NotContainsKey[T any, K comparable](expected K) Kleisli[map[K]T] {
|
||||
//
|
||||
// func TestThat(t *testing.T) {
|
||||
// // Test if a number is positive
|
||||
// isPositive := func(n int) bool { return n > 0 }
|
||||
// isPositive := N.MoreThan(0)
|
||||
// assert.That(isPositive)(42)(t) // Passes
|
||||
// assert.That(isPositive)(-5)(t) // Fails
|
||||
//
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/assert"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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](
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -44,11 +44,11 @@ var (
|
||||
)
|
||||
|
||||
// Close closes an object
|
||||
func Close[C io.Closer](c C) RIOE.ReaderIOResult[any] {
|
||||
func Close[C io.Closer](c C) RIOE.ReaderIOResult[struct{}] {
|
||||
return F.Pipe2(
|
||||
c,
|
||||
IOEF.Close[C],
|
||||
RIOE.FromIOEither[any],
|
||||
RIOE.FromIOEither[struct{}],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
51
v2/context/readerioresult/filter.go
Normal file
51
v2/context/readerioresult/filter.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||
)
|
||||
|
||||
// FilterOrElse filters a ReaderIOResult value based on a predicate.
|
||||
// This is a convenience wrapper around readerioresult.FilterOrElse that fixes
|
||||
// the context type to context.Context.
|
||||
//
|
||||
// If the predicate returns true for the Right value, it passes through unchanged.
|
||||
// If the predicate returns false, it transforms the Right value into a Left (error) using onFalse.
|
||||
// Left values are passed through unchanged.
|
||||
//
|
||||
// Parameters:
|
||||
// - pred: A predicate function that tests the Right value
|
||||
// - onFalse: A function that converts the failing value into an error
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that filters ReaderIOResult values based on the predicate
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Validate that a number is positive
|
||||
// isPositive := N.MoreThan(0)
|
||||
// onNegative := func(n int) error { return fmt.Errorf("%d is not positive", n) }
|
||||
//
|
||||
// filter := readerioresult.FilterOrElse(isPositive, onNegative)
|
||||
// result := filter(readerioresult.Right(42))(context.Background())()
|
||||
//
|
||||
//go:inline
|
||||
func FilterOrElse[A any](pred Predicate[A], onFalse func(A) error) Operator[A, A] {
|
||||
return RIOR.FilterOrElse[context.Context](pred, onFalse)
|
||||
}
|
||||
@@ -235,7 +235,7 @@ func TestApPar(t *testing.T) {
|
||||
func TestFromPredicate(t *testing.T) {
|
||||
t.Run("Predicate true", func(t *testing.T) {
|
||||
pred := FromPredicate(
|
||||
func(x int) bool { return x > 0 },
|
||||
N.MoreThan(0),
|
||||
func(x int) error { return fmt.Errorf("value %d is not positive", x) },
|
||||
)
|
||||
result := pred(5)
|
||||
@@ -244,7 +244,7 @@ func TestFromPredicate(t *testing.T) {
|
||||
|
||||
t.Run("Predicate false", func(t *testing.T) {
|
||||
pred := FromPredicate(
|
||||
func(x int) bool { return x > 0 },
|
||||
N.MoreThan(0),
|
||||
func(x int) error { return fmt.Errorf("value %d is not positive", x) },
|
||||
)
|
||||
result := pred(-5)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readereither"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
@@ -140,4 +141,6 @@ type (
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
|
||||
Trampoline[B, L any] = tailrec.Trampoline[B, L]
|
||||
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
)
|
||||
|
||||
51
v2/context/readerresult/filter.go
Normal file
51
v2/context/readerresult/filter.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
RR "github.com/IBM/fp-go/v2/readerresult"
|
||||
)
|
||||
|
||||
// FilterOrElse filters a ReaderResult value based on a predicate.
|
||||
// This is a convenience wrapper around readerresult.FilterOrElse that fixes
|
||||
// the context type to context.Context.
|
||||
//
|
||||
// If the predicate returns true for the Right value, it passes through unchanged.
|
||||
// If the predicate returns false, it transforms the Right value into a Left (error) using onFalse.
|
||||
// Left values are passed through unchanged.
|
||||
//
|
||||
// Parameters:
|
||||
// - pred: A predicate function that tests the Right value
|
||||
// - onFalse: A function that converts the failing value into an error
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that filters ReaderResult values based on the predicate
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Validate that a number is positive
|
||||
// isPositive := N.MoreThan(0)
|
||||
// onNegative := func(n int) error { return fmt.Errorf("%d is not positive", n) }
|
||||
//
|
||||
// filter := readerresult.FilterOrElse(isPositive, onNegative)
|
||||
// result := filter(readerresult.Right(42))(context.Background())
|
||||
//
|
||||
//go:inline
|
||||
func FilterOrElse[A any](pred Predicate[A], onFalse func(A) error) Operator[A, A] {
|
||||
return RR.FilterOrElse[context.Context](pred, onFalse)
|
||||
}
|
||||
@@ -73,6 +73,48 @@ func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) Kleisli[A, A
|
||||
return readereither.FromPredicate[context.Context](pred, onFalse)
|
||||
}
|
||||
|
||||
// OrElse recovers from a Left (error) by providing an alternative computation with access to context.Context.
|
||||
// If the ReaderResult is Right, it returns the value unchanged.
|
||||
// If the ReaderResult is Left, it applies the provided function to the error value,
|
||||
// which returns a new ReaderResult that replaces the original.
|
||||
//
|
||||
// This is useful for error recovery, fallback logic, or chaining alternative computations
|
||||
// that need access to the context (for cancellation, deadlines, or values).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Recover with context-aware fallback
|
||||
// recover := readerresult.OrElse(func(err error) readerresult.ReaderResult[int] {
|
||||
// if err.Error() == "not found" {
|
||||
// return func(ctx context.Context) result.Result[int] {
|
||||
// // Could check ctx.Err() here
|
||||
// return result.Of(42)
|
||||
// }
|
||||
// }
|
||||
// return readerresult.Left[int](err)
|
||||
// })
|
||||
//
|
||||
// OrElse recovers from a Left (error) by providing an alternative computation.
|
||||
// If the ReaderResult is Right, it returns the value unchanged.
|
||||
// If the ReaderResult is Left, it applies the provided function to the error value,
|
||||
// which returns a new ReaderResult that replaces the original.
|
||||
//
|
||||
// This is useful for error recovery, fallback logic, or chaining alternative computations
|
||||
// in the context of Reader computations with context.Context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Recover from specific errors with fallback values
|
||||
// recover := readerresult.OrElse(func(err error) readerresult.ReaderResult[int] {
|
||||
// if err.Error() == "not found" {
|
||||
// return readerresult.Of[int](0) // default value
|
||||
// }
|
||||
// return readerresult.Left[int](err) // propagate other errors
|
||||
// })
|
||||
// result := recover(readerresult.Left[int](errors.New("not found")))(ctx) // Right(0)
|
||||
// result := recover(readerresult.Of(42))(ctx) // Right(42) - unchanged
|
||||
//
|
||||
//go:inline
|
||||
func OrElse[A any](onLeft Kleisli[error, A]) Kleisli[ReaderResult[A], A] {
|
||||
return readereither.OrElse(F.Flow2(onLeft, WithContext))
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ package readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
@@ -313,3 +314,69 @@ func TestMonadChainTo(t *testing.T) {
|
||||
assert.False(t, secondExecuted, "second reader should not be executed on error")
|
||||
})
|
||||
}
|
||||
|
||||
func TestOrElse(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test OrElse with Right - should pass through unchanged
|
||||
t.Run("Right value unchanged", func(t *testing.T) {
|
||||
rightValue := Of(42)
|
||||
recover := OrElse(func(err error) ReaderResult[int] {
|
||||
return Left[int](errors.New("should not be called"))
|
||||
})
|
||||
res := recover(rightValue)(ctx)
|
||||
assert.Equal(t, E.Of[error](42), res)
|
||||
})
|
||||
|
||||
// Test OrElse with Left - should recover with fallback
|
||||
t.Run("Left value recovered", func(t *testing.T) {
|
||||
leftValue := Left[int](errors.New("not found"))
|
||||
recoverWithFallback := OrElse(func(err error) ReaderResult[int] {
|
||||
if err.Error() == "not found" {
|
||||
return func(ctx context.Context) E.Either[error, int] {
|
||||
return E.Of[error](99)
|
||||
}
|
||||
}
|
||||
return Left[int](err)
|
||||
})
|
||||
res := recoverWithFallback(leftValue)(ctx)
|
||||
assert.Equal(t, E.Of[error](99), res)
|
||||
})
|
||||
|
||||
// Test OrElse with Left - should propagate other errors
|
||||
t.Run("Left value propagated", func(t *testing.T) {
|
||||
leftValue := Left[int](errors.New("fatal error"))
|
||||
recoverWithFallback := OrElse(func(err error) ReaderResult[int] {
|
||||
if err.Error() == "not found" {
|
||||
return Of(99)
|
||||
}
|
||||
return Left[int](err)
|
||||
})
|
||||
res := recoverWithFallback(leftValue)(ctx)
|
||||
assert.True(t, E.IsLeft(res))
|
||||
val, err := E.UnwrapError(res)
|
||||
assert.Equal(t, 0, val)
|
||||
assert.Equal(t, "fatal error", err.Error())
|
||||
})
|
||||
|
||||
// Test OrElse with context-aware recovery
|
||||
t.Run("Context-aware recovery", func(t *testing.T) {
|
||||
type ctxKey string
|
||||
ctxWithValue := context.WithValue(ctx, ctxKey("fallback"), 123)
|
||||
|
||||
leftValue := Left[int](errors.New("use fallback"))
|
||||
ctxRecover := OrElse(func(err error) ReaderResult[int] {
|
||||
if err.Error() == "use fallback" {
|
||||
return func(ctx context.Context) E.Either[error, int] {
|
||||
if val := ctx.Value(ctxKey("fallback")); val != nil {
|
||||
return E.Of[error](val.(int))
|
||||
}
|
||||
return E.Left[int](errors.New("no fallback"))
|
||||
}
|
||||
}
|
||||
return Left[int](err)
|
||||
})
|
||||
res := ctxRecover(leftValue)(ctxWithValue)
|
||||
assert.Equal(t, E.Of[error](123), res)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -48,6 +48,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readereither"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
@@ -68,4 +69,5 @@ type (
|
||||
Prism[S, T any] = prism.Prism[S, T]
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
Trampoline[A, B any] = tailrec.Trampoline[A, B]
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
)
|
||||
|
||||
55
v2/context/statereaderioresult/filter.go
Normal file
55
v2/context/statereaderioresult/filter.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2024 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package statereaderioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/statereaderioeither"
|
||||
)
|
||||
|
||||
// FilterOrElse filters a StateReaderIOResult value based on a predicate.
|
||||
// This is a convenience wrapper around statereaderioeither.FilterOrElse that fixes
|
||||
// the context type to context.Context and the error type to error.
|
||||
//
|
||||
// If the predicate returns true for the Right value, it passes through unchanged.
|
||||
// If the predicate returns false, it transforms the Right value into a Left (error) using onFalse.
|
||||
// Left values are passed through unchanged.
|
||||
//
|
||||
// Parameters:
|
||||
// - pred: A predicate function that tests the Right value
|
||||
// - onFalse: A function that converts the failing value into an error
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that filters StateReaderIOResult values based on the predicate
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type AppState struct {
|
||||
// Counter int
|
||||
// }
|
||||
//
|
||||
// // Validate that a number is positive
|
||||
// isPositive := N.MoreThan(0)
|
||||
// onNegative := func(n int) error { return fmt.Errorf("%d is not positive", n) }
|
||||
//
|
||||
// filter := statereaderioresult.FilterOrElse[AppState](isPositive, onNegative)
|
||||
// result := filter(statereaderioresult.Right[AppState](42))(AppState{})(context.Background())()
|
||||
//
|
||||
//go:inline
|
||||
func FilterOrElse[S, A any](pred Predicate[A], onFalse func(A) error) Operator[S, A, A] {
|
||||
return statereaderioeither.FilterOrElse[S, context.Context](pred, onFalse)
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/optics/iso/lens"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/IBM/fp-go/v2/state"
|
||||
@@ -81,4 +82,6 @@ type (
|
||||
// Operator represents a function that transforms one StateReaderIOResult into another.
|
||||
// This is commonly used for building composable operations via Map, Chain, etc.
|
||||
Operator[S, A, B any] = Reader[StateReaderIOResult[S, A], StateReaderIOResult[S, B]]
|
||||
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -394,12 +394,12 @@ func UnwrapError[A any](ma Either[error, A]) (A, error) {
|
||||
// Example:
|
||||
//
|
||||
// isPositive := either.FromPredicate(
|
||||
// func(x int) bool { return x > 0 },
|
||||
// N.MoreThan(0),
|
||||
// func(x int) error { return errors.New("not positive") },
|
||||
// )
|
||||
// result := isPositive(42) // Right(42)
|
||||
// result := isPositive(-1) // Left(error)
|
||||
func FromPredicate[E, A any](pred func(A) bool, onFalse func(A) E) func(A) Either[E, A] {
|
||||
func FromPredicate[E, A any](pred Predicate[A], onFalse func(A) E) Kleisli[E, A, A] {
|
||||
return func(a A) Either[E, A] {
|
||||
if pred(a) {
|
||||
return Right[E](a)
|
||||
@@ -416,7 +416,7 @@ func FromPredicate[E, A any](pred func(A) bool, onFalse func(A) E) func(A) Eithe
|
||||
// result := either.FromNillable[int](errors.New("nil"))(ptr) // Left(error)
|
||||
// val := 42
|
||||
// result := either.FromNillable[int](errors.New("nil"))(&val) // Right(&42)
|
||||
func FromNillable[A, E any](e E) func(*A) Either[E, *A] {
|
||||
func FromNillable[A, E any](e E) Kleisli[E, *A, *A] {
|
||||
return FromPredicate(F.IsNonNil[A], F.Constant1[*A](e))
|
||||
}
|
||||
|
||||
@@ -450,7 +450,7 @@ func Reduce[E, A, B any](f func(B, A) B, initial B) func(Either[E, A]) B {
|
||||
// return either.Right[string](99)
|
||||
// })
|
||||
// result := alternative(either.Left[int](errors.New("fail"))) // Right(99)
|
||||
func AltW[E, E1, A any](that Lazy[Either[E1, A]]) func(Either[E, A]) Either[E1, A] {
|
||||
func AltW[E, E1, A any](that Lazy[Either[E1, A]]) Kleisli[E1, Either[E, A], A] {
|
||||
return Fold(F.Ignore1of1[E](that), Right[E1, A])
|
||||
}
|
||||
|
||||
@@ -466,16 +466,29 @@ func Alt[E, A any](that Lazy[Either[E, A]]) Operator[E, A, A] {
|
||||
return AltW[E](that)
|
||||
}
|
||||
|
||||
// OrElse recovers from a Left by providing an alternative computation.
|
||||
// OrElse recovers from a Left (error) by providing an alternative computation.
|
||||
// If the Either is Right, it returns the value unchanged.
|
||||
// If the Either is Left, it applies the provided function to the error value,
|
||||
// which returns a new Either that replaces the original.
|
||||
//
|
||||
// This is useful for error recovery, fallback logic, or chaining alternative computations.
|
||||
// The error type can be widened from E1 to E2, allowing transformation of error types.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Recover from specific errors with fallback values
|
||||
// recover := either.OrElse(func(err error) either.Either[error, int] {
|
||||
// return either.Right[error](0) // default value
|
||||
// if err.Error() == "not found" {
|
||||
// return either.Right[error](0) // default value
|
||||
// }
|
||||
// return either.Left[int](err) // propagate other errors
|
||||
// })
|
||||
// result := recover(either.Left[int](errors.New("fail"))) // Right(0)
|
||||
func OrElse[E, A any](onLeft Kleisli[E, E, A]) Operator[E, A, A] {
|
||||
return Fold(onLeft, Of[E, A])
|
||||
// result := recover(either.Left[int](errors.New("not found"))) // Right(0)
|
||||
// result := recover(either.Right[error](42)) // Right(42) - unchanged
|
||||
//
|
||||
//go:inline
|
||||
func OrElse[E1, E2, A any](onLeft Kleisli[E2, E1, A]) Kleisli[E2, Either[E1, A], A] {
|
||||
return Fold(onLeft, Of[E2, A])
|
||||
}
|
||||
|
||||
// ToType attempts to convert an any value to a specific type, returning Either.
|
||||
|
||||
@@ -160,6 +160,7 @@ func TestToError(t *testing.T) {
|
||||
|
||||
// Test OrElse
|
||||
func TestOrElse(t *testing.T) {
|
||||
// Test basic recovery from Left
|
||||
recover := OrElse(func(e error) Either[error, int] {
|
||||
return Right[error](0)
|
||||
})
|
||||
@@ -167,8 +168,85 @@ func TestOrElse(t *testing.T) {
|
||||
result := recover(Left[int](errors.New("error")))
|
||||
assert.Equal(t, Right[error](0), result)
|
||||
|
||||
// Test Right value passes through unchanged
|
||||
result = recover(Right[error](42))
|
||||
assert.Equal(t, Right[error](42), result)
|
||||
|
||||
// Test selective recovery - recover some errors, propagate others
|
||||
selectiveRecover := OrElse(func(err error) Either[error, int] {
|
||||
if err.Error() == "not found" {
|
||||
return Right[error](0) // default value for "not found"
|
||||
}
|
||||
return Left[int](err) // propagate other errors
|
||||
})
|
||||
assert.Equal(t, Right[error](0), selectiveRecover(Left[int](errors.New("not found"))))
|
||||
permissionErr := errors.New("permission denied")
|
||||
assert.Equal(t, Left[int](permissionErr), selectiveRecover(Left[int](permissionErr)))
|
||||
|
||||
// Test chaining multiple OrElse operations
|
||||
firstRecover := OrElse(func(err error) Either[error, int] {
|
||||
if err.Error() == "error1" {
|
||||
return Right[error](1)
|
||||
}
|
||||
return Left[int](err)
|
||||
})
|
||||
secondRecover := OrElse(func(err error) Either[error, int] {
|
||||
if err.Error() == "error2" {
|
||||
return Right[error](2)
|
||||
}
|
||||
return Left[int](err)
|
||||
})
|
||||
assert.Equal(t, Right[error](1), F.Pipe1(Left[int](errors.New("error1")), firstRecover))
|
||||
assert.Equal(t, Right[error](2), F.Pipe1(Left[int](errors.New("error2")), F.Flow2(firstRecover, secondRecover)))
|
||||
}
|
||||
|
||||
// Test OrElseW
|
||||
func TestOrElseW(t *testing.T) {
|
||||
type ValidationError string
|
||||
type AppError int
|
||||
|
||||
// Test with Right value - should return Right with widened error type
|
||||
rightValue := Right[ValidationError]("success")
|
||||
recoverValidation := OrElse(func(ve ValidationError) Either[AppError, string] {
|
||||
return Left[string](AppError(400))
|
||||
})
|
||||
result := recoverValidation(rightValue)
|
||||
assert.True(t, IsRight(result))
|
||||
assert.Equal(t, "success", F.Pipe1(result, GetOrElse(F.Constant1[AppError](""))))
|
||||
|
||||
// Test with Left value - should apply recovery with new error type
|
||||
leftValue := Left[string](ValidationError("invalid input"))
|
||||
result = recoverValidation(leftValue)
|
||||
assert.True(t, IsLeft(result))
|
||||
_, leftVal := Unwrap(result)
|
||||
assert.Equal(t, AppError(400), leftVal)
|
||||
|
||||
// Test error type conversion - ValidationError to AppError
|
||||
convertError := OrElse(func(ve ValidationError) Either[AppError, int] {
|
||||
return Left[int](AppError(len(ve)))
|
||||
})
|
||||
converted := convertError(Left[int](ValidationError("short")))
|
||||
assert.True(t, IsLeft(converted))
|
||||
_, leftConv := Unwrap(converted)
|
||||
assert.Equal(t, AppError(5), leftConv)
|
||||
|
||||
// Test recovery to Right with widened error type
|
||||
recoverToRight := OrElse(func(ve ValidationError) Either[AppError, int] {
|
||||
if ve == "recoverable" {
|
||||
return Right[AppError](99)
|
||||
}
|
||||
return Left[int](AppError(500))
|
||||
})
|
||||
assert.Equal(t, Right[AppError](99), recoverToRight(Left[int](ValidationError("recoverable"))))
|
||||
assert.True(t, IsLeft(recoverToRight(Left[int](ValidationError("fatal")))))
|
||||
|
||||
// Test that Right values are preserved with widened error type
|
||||
preservedRight := Right[ValidationError](42)
|
||||
preserveRecover := OrElse(func(ve ValidationError) Either[AppError, int] {
|
||||
return Left[int](AppError(999))
|
||||
})
|
||||
preserved := preserveRecover(preservedRight)
|
||||
assert.Equal(t, Right[AppError](42), preserved)
|
||||
}
|
||||
|
||||
// Test ToType
|
||||
|
||||
38
v2/either/filter.go
Normal file
38
v2/either/filter.go
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package either
|
||||
|
||||
// FilterOrElse filters an Either value based on a predicate.
|
||||
// If the Either is Right and the predicate returns true, returns the original Right.
|
||||
// If the Either is Right and the predicate returns false, returns Left with the error from onFalse.
|
||||
// If the Either is Left, returns the original Left without applying the predicate.
|
||||
//
|
||||
// This is useful for adding validation to Right values, converting them to Left if they don't meet certain criteria.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// isPositive := N.MoreThan(0)
|
||||
// onNegative := func(x int) error { return fmt.Errorf("%d is not positive", x) }
|
||||
// filter := either.FilterOrElse(isPositive, onNegative)
|
||||
//
|
||||
// result1 := filter(either.Right[error](5)) // Right(5)
|
||||
// result2 := filter(either.Right[error](-3)) // Left(error: "-3 is not positive")
|
||||
// result3 := filter(either.Left[int](someError)) // Left(someError)
|
||||
//
|
||||
//go:inline
|
||||
func FilterOrElse[E, A any](pred Predicate[A], onFalse func(A) E) Operator[E, A, A] {
|
||||
return Chain(FromPredicate(pred, onFalse))
|
||||
}
|
||||
143
v2/either/filter_test.go
Normal file
143
v2/either/filter_test.go
Normal file
@@ -0,0 +1,143 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package either
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFilterOrElse(t *testing.T) {
|
||||
// Test with positive predicate
|
||||
isPositive := N.MoreThan(0)
|
||||
onNegative := func(x int) error { return fmt.Errorf("%d is not positive", x) }
|
||||
filter := FilterOrElse(isPositive, onNegative)
|
||||
|
||||
// Test Right value that passes predicate
|
||||
result := filter(Right[error](5))
|
||||
assert.Equal(t, Right[error](5), result)
|
||||
|
||||
// Test Right value that fails predicate
|
||||
result = filter(Right[error](-3))
|
||||
assert.True(t, IsLeft(result))
|
||||
left, _ := UnwrapError(result)
|
||||
assert.Equal(t, 0, left) // default value for int
|
||||
|
||||
// Test Right value at boundary (zero)
|
||||
result = filter(Right[error](0))
|
||||
assert.True(t, IsLeft(result))
|
||||
|
||||
// Test Left value (should pass through unchanged)
|
||||
originalError := errors.New("original error")
|
||||
result = filter(Left[int](originalError))
|
||||
assert.Equal(t, Left[int](originalError), result)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_StringValidation(t *testing.T) {
|
||||
// Test with string length validation
|
||||
isNotEmpty := func(s string) bool { return len(s) > 0 }
|
||||
onEmpty := func(s string) error { return errors.New("string is empty") }
|
||||
filter := FilterOrElse(isNotEmpty, onEmpty)
|
||||
|
||||
// Test non-empty string
|
||||
result := filter(Right[error]("hello"))
|
||||
assert.Equal(t, Right[error]("hello"), result)
|
||||
|
||||
// Test empty string
|
||||
result = filter(Right[error](""))
|
||||
assert.True(t, IsLeft(result))
|
||||
|
||||
// Test Left value
|
||||
originalError := errors.New("validation error")
|
||||
result = filter(Left[string](originalError))
|
||||
assert.Equal(t, Left[string](originalError), result)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_ComplexPredicate(t *testing.T) {
|
||||
// Test with range validation
|
||||
inRange := func(x int) bool { return x >= 10 && x <= 100 }
|
||||
outOfRange := func(x int) error { return fmt.Errorf("%d is out of range [10, 100]", x) }
|
||||
filter := FilterOrElse(inRange, outOfRange)
|
||||
|
||||
// Test value in range
|
||||
result := filter(Right[error](50))
|
||||
assert.Equal(t, Right[error](50), result)
|
||||
|
||||
// Test value below range
|
||||
result = filter(Right[error](5))
|
||||
assert.True(t, IsLeft(result))
|
||||
|
||||
// Test value above range
|
||||
result = filter(Right[error](150))
|
||||
assert.True(t, IsLeft(result))
|
||||
|
||||
// Test boundary values
|
||||
result = filter(Right[error](10))
|
||||
assert.Equal(t, Right[error](10), result)
|
||||
|
||||
result = filter(Right[error](100))
|
||||
assert.Equal(t, Right[error](100), result)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_ChainedFilters(t *testing.T) {
|
||||
// Test chaining multiple filters
|
||||
isPositive := N.MoreThan(0)
|
||||
onNegative := func(x int) error { return fmt.Errorf("%d is not positive", x) }
|
||||
|
||||
isEven := func(x int) bool { return x%2 == 0 }
|
||||
onOdd := func(x int) error { return fmt.Errorf("%d is not even", x) }
|
||||
|
||||
filterPositive := FilterOrElse(isPositive, onNegative)
|
||||
filterEven := FilterOrElse(isEven, onOdd)
|
||||
|
||||
// Test value that passes both filters
|
||||
result := filterEven(filterPositive(Right[error](4)))
|
||||
assert.Equal(t, Right[error](4), result)
|
||||
|
||||
// Test value that fails first filter
|
||||
result = filterEven(filterPositive(Right[error](-2)))
|
||||
assert.True(t, IsLeft(result))
|
||||
|
||||
// Test value that passes first but fails second filter
|
||||
result = filterEven(filterPositive(Right[error](3)))
|
||||
assert.True(t, IsLeft(result))
|
||||
}
|
||||
|
||||
func TestFilterOrElse_WithStructs(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
// Test with struct validation
|
||||
isAdult := func(u User) bool { return u.Age >= 18 }
|
||||
onMinor := func(u User) error { return fmt.Errorf("%s is not an adult (age: %d)", u.Name, u.Age) }
|
||||
filter := FilterOrElse(isAdult, onMinor)
|
||||
|
||||
// Test adult user
|
||||
adult := User{Name: "Alice", Age: 25}
|
||||
result := filter(Right[error](adult))
|
||||
assert.Equal(t, Right[error](adult), result)
|
||||
|
||||
// Test minor user
|
||||
minor := User{Name: "Bob", Age: 16}
|
||||
result = filter(Right[error](minor))
|
||||
assert.True(t, IsLeft(result))
|
||||
}
|
||||
@@ -48,7 +48,7 @@ import (
|
||||
// eqError := eq.FromStrictEquals[error]()
|
||||
//
|
||||
// ab := strconv.Itoa
|
||||
// bc := func(s string) bool { return len(s) > 0 }
|
||||
// bc := S.IsNonEmpty
|
||||
//
|
||||
// testing.AssertLaws(t, eqError, eqInt, eqString, eq.FromStrictEquals[bool](), ab, bc)(42)
|
||||
// }
|
||||
|
||||
@@ -21,18 +21,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]
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
//
|
||||
// Working with predicates:
|
||||
//
|
||||
// isPositive := func(n int) bool { return n > 0 }
|
||||
// isPositive := N.MoreThan(0)
|
||||
// isEven := func(n int) bool { return n%2 == 0 }
|
||||
//
|
||||
// classify := Ternary(
|
||||
|
||||
@@ -35,7 +35,7 @@ package function
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// isPositive := func(n int) bool { return n > 0 }
|
||||
// isPositive := N.MoreThan(0)
|
||||
// double := N.Mul(2)
|
||||
// negate := func(n int) int { return -n }
|
||||
//
|
||||
@@ -45,7 +45,7 @@ package function
|
||||
//
|
||||
// // Classify numbers
|
||||
// classify := Ternary(
|
||||
// func(n int) bool { return n > 0 },
|
||||
// N.MoreThan(0),
|
||||
// Constant1[int, string]("positive"),
|
||||
// Constant1[int, string]("non-positive"),
|
||||
// )
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
package identity
|
||||
|
||||
type (
|
||||
Kleisli[A, B any] = func(A) B
|
||||
// Kleisli represents a Kleisli arrow for the Identity monad.
|
||||
// It's simply a function from A to B, as Identity has no computational context.
|
||||
Kleisli[A, B any] = func(A) B
|
||||
|
||||
// Operator represents a function that transforms values.
|
||||
// In the Identity monad, it's equivalent to Kleisli since there's no wrapping context.
|
||||
Operator[A, B any] = Kleisli[A, B]
|
||||
)
|
||||
|
||||
47
v2/idiomatic/context/readerresult/filter.go
Normal file
47
v2/idiomatic/context/readerresult/filter.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerresult
|
||||
|
||||
import "github.com/IBM/fp-go/v2/either"
|
||||
|
||||
// FilterOrElse filters a context-aware ReaderResult value based on a predicate in an idiomatic style.
|
||||
// If the ReaderResult computation succeeds and the predicate returns true, returns the original success value.
|
||||
// If the ReaderResult computation succeeds and the predicate returns false, returns an error with the error from onFalse.
|
||||
// If the ReaderResult computation fails, returns the original error without applying the predicate.
|
||||
//
|
||||
// This is the idiomatic version that returns an Operator for use in method chaining.
|
||||
// It's useful for adding validation to successful context-aware computations, converting them to errors if they don't meet certain criteria.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// CRR "github.com/IBM/fp-go/v2/idiomatic/context/readerresult"
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// )
|
||||
//
|
||||
// isPositive := N.MoreThan(0)
|
||||
// onNegative := func(x int) error { return fmt.Errorf("%d is not positive", x) }
|
||||
//
|
||||
// result := CRR.Of(5).
|
||||
// Pipe(CRR.FilterOrElse(isPositive, onNegative))(ctx) // Ok(5)
|
||||
//
|
||||
// result2 := CRR.Of(-3).
|
||||
// Pipe(CRR.FilterOrElse(isPositive, onNegative))(ctx) // Error(error: "-3 is not positive")
|
||||
//
|
||||
//go:inline
|
||||
func FilterOrElse[A any](pred Predicate[A], onFalse func(A) error) Operator[A, A] {
|
||||
return ChainEitherK(either.FromPredicate(pred, onFalse))
|
||||
}
|
||||
@@ -393,6 +393,26 @@ func GetOrElse[A any](onLeft reader.Kleisli[context.Context, error, A]) func(Rea
|
||||
return RR.GetOrElse(onLeft)
|
||||
}
|
||||
|
||||
// OrElse recovers from a Left (error) by providing an alternative computation.
|
||||
// If the ReaderResult is Right, it returns the value unchanged.
|
||||
// If the ReaderResult is Left, it applies the provided function to the error value,
|
||||
// which returns a new ReaderResult that replaces the original.
|
||||
//
|
||||
// This is the idiomatic version that works with context.Context-based ReaderResult.
|
||||
// This is useful for error recovery, fallback logic, or chaining alternative computations.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Recover from specific errors with fallback values
|
||||
// recover := readerresult.OrElse(func(err error) readerresult.ReaderResult[int] {
|
||||
// if err.Error() == "not found" {
|
||||
// return readerresult.Of[int](0) // default value
|
||||
// }
|
||||
// return readerresult.Left[int](err) // propagate other errors
|
||||
// })
|
||||
// result := recover(readerresult.Left[int](errors.New("not found")))(ctx) // Right(0)
|
||||
// result := recover(readerresult.Of(42))(ctx) // Right(42) - unchanged
|
||||
//
|
||||
//go:inline
|
||||
func OrElse[A any](onLeft Kleisli[error, A]) Operator[A, A] {
|
||||
return RR.OrElse(WithContextK(onLeft))
|
||||
|
||||
120
v2/idiomatic/context/readerresult/rec.go
Normal file
120
v2/idiomatic/context/readerresult/rec.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
597
v2/idiomatic/context/readerresult/rec_test.go
Normal file
597
v2/idiomatic/context/readerresult/rec_test.go
Normal 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
|
||||
}
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -460,7 +460,7 @@
|
||||
// 5. Use FromPredicate for validation:
|
||||
//
|
||||
// positiveInt := result.FromPredicate(
|
||||
// func(x int) bool { return x > 0 },
|
||||
// N.MoreThan(0),
|
||||
// func(x int) error { return fmt.Errorf("%d is not positive", x) },
|
||||
// )
|
||||
//
|
||||
|
||||
47
v2/idiomatic/ioresult/filter.go
Normal file
47
v2/idiomatic/ioresult/filter.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ioresult
|
||||
|
||||
import "github.com/IBM/fp-go/v2/idiomatic/result"
|
||||
|
||||
// FilterOrElse filters an IOResult value based on a predicate in an idiomatic style.
|
||||
// If the IOResult computation succeeds and the predicate returns true, returns the original success value.
|
||||
// If the IOResult computation succeeds and the predicate returns false, returns an error with the error from onFalse.
|
||||
// If the IOResult computation fails, returns the original error without applying the predicate.
|
||||
//
|
||||
// This is the idiomatic version that returns an Operator for use in method chaining.
|
||||
// It's useful for adding validation to successful IO computations, converting them to errors if they don't meet certain criteria.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// IO "github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// )
|
||||
//
|
||||
// isPositive := N.MoreThan(0)
|
||||
// onNegative := func(x int) error { return fmt.Errorf("%d is not positive", x) }
|
||||
//
|
||||
// result := IO.Of(5).
|
||||
// Pipe(IO.FilterOrElse(isPositive, onNegative))() // Ok(5)
|
||||
//
|
||||
// result2 := IO.Of(-3).
|
||||
// Pipe(IO.FilterOrElse(isPositive, onNegative))() // Error(error: "-3 is not positive")
|
||||
//
|
||||
//go:inline
|
||||
func FilterOrElse[A any](pred Predicate[A], onFalse func(A) error) Operator[A, A] {
|
||||
return ChainResultK(result.FromPredicate(pred, onFalse))
|
||||
}
|
||||
85
v2/idiomatic/ioresult/filter_test.go
Normal file
85
v2/idiomatic/ioresult/filter_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ioresult
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFilterOrElse(t *testing.T) {
|
||||
// Test with positive predicate
|
||||
isPositive := N.MoreThan(0)
|
||||
onNegative := func(x int) error { return fmt.Errorf("%d is not positive", x) }
|
||||
|
||||
// Test value that passes predicate
|
||||
result, err := F.Pipe2(5, Of, FilterOrElse(isPositive, onNegative))()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 5, result)
|
||||
|
||||
// Test value that fails predicate
|
||||
_, err = F.Pipe2(-3, Of, FilterOrElse(isPositive, onNegative))()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "-3 is not positive", err.Error())
|
||||
|
||||
// Test error value (should pass through unchanged)
|
||||
originalError := errors.New("original error")
|
||||
_, err = F.Pipe2(originalError, Left[int], FilterOrElse(isPositive, onNegative))()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, originalError, err)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_WithChain(t *testing.T) {
|
||||
// Test FilterOrElse in a chain with other IO operations
|
||||
isPositive := N.MoreThan(0)
|
||||
onNegative := func(x int) error { return fmt.Errorf("%d is not positive", x) }
|
||||
|
||||
double := func(x int) (int, error) { return x * 2, nil }
|
||||
|
||||
// Test successful chain
|
||||
result, err := F.Pipe3(5, Of, FilterOrElse(isPositive, onNegative), ChainResultK(double))()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 10, result)
|
||||
|
||||
// Test chain with filter failure
|
||||
_, err = F.Pipe3(-5, Of, FilterOrElse(isPositive, onNegative), ChainResultK(double))()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not positive")
|
||||
}
|
||||
|
||||
func TestFilterOrElse_MultipleFilters(t *testing.T) {
|
||||
// Test chaining multiple filters
|
||||
isPositive := N.MoreThan(0)
|
||||
onNegative := func(x int) error { return fmt.Errorf("%d is not positive", x) }
|
||||
|
||||
isEven := func(x int) bool { return x%2 == 0 }
|
||||
onOdd := func(x int) error { return fmt.Errorf("%d is not even", x) }
|
||||
|
||||
// Test value that passes both filters
|
||||
result, err := F.Pipe3(4, Of, FilterOrElse(isPositive, onNegative), FilterOrElse(isEven, onOdd))()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 4, result)
|
||||
|
||||
// Test value that fails second filter
|
||||
_, err = F.Pipe3(3, Of, FilterOrElse(isPositive, onNegative), FilterOrElse(isEven, onOdd))()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not even")
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
@@ -36,4 +37,6 @@ type (
|
||||
// Operator represents a transformation from IOResult[A] to IOResult[B].
|
||||
// It is commonly used in function composition pipelines.
|
||||
Operator[A, B any] = Kleisli[IOResult[A], B]
|
||||
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
)
|
||||
|
||||
@@ -73,7 +73,7 @@ func BenchmarkChain(b *testing.B) {
|
||||
|
||||
func BenchmarkFilter(b *testing.B) {
|
||||
v, ok := Some(42)
|
||||
filter := Filter(func(x int) bool { return x > 0 })
|
||||
filter := Filter(N.MoreThan(0))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -152,7 +152,7 @@ func BenchmarkDoBind(b *testing.B) {
|
||||
|
||||
// Benchmark conversions
|
||||
func BenchmarkFromPredicate(b *testing.B) {
|
||||
pred := FromPredicate(func(x int) bool { return x > 0 })
|
||||
pred := FromPredicate(N.MoreThan(0))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
//
|
||||
// Filter keeps values that satisfy a predicate:
|
||||
//
|
||||
// isPositive := Filter(func(x int) bool { return x > 0 })
|
||||
// isPositive := Filter(N.MoreThan(0))
|
||||
// result := isPositive(Some(5)) // (5, true)
|
||||
// result := isPositive(Some(-1)) // (0, false)
|
||||
//
|
||||
@@ -127,7 +127,7 @@
|
||||
//
|
||||
// Convert predicates to Options:
|
||||
//
|
||||
// isPositive := FromPredicate(func(n int) bool { return n > 0 })
|
||||
// isPositive := FromPredicate(N.MoreThan(0))
|
||||
// result := isPositive(5) // (5, true)
|
||||
// result := isPositive(-1) // (0, false)
|
||||
//
|
||||
|
||||
@@ -47,7 +47,7 @@ import (
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// isPositive := FromPredicate(func(n int) bool { return n > 0 })
|
||||
// isPositive := FromPredicate(N.MoreThan(0))
|
||||
// result := isPositive(5) // Some(5)
|
||||
// result := isPositive(-1) // None
|
||||
func FromPredicate[A any](pred func(A) bool) Kleisli[A, A] {
|
||||
@@ -330,7 +330,7 @@ func Reduce[A, B any](f func(B, A) B, initial B) func(A, bool) B {
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// isPositive := Filter(func(x int) bool { return x > 0 })
|
||||
// isPositive := Filter(N.MoreThan(0))
|
||||
// result := isPositive(Some(5)) // Some(5)
|
||||
// result := isPositive(Some(-1)) // None
|
||||
// result := isPositive(None[int]()) // None
|
||||
|
||||
@@ -130,20 +130,20 @@ func TestChainFirst(t *testing.T) {
|
||||
// Test Filter
|
||||
func TestFilter(t *testing.T) {
|
||||
t.Run("positive case - predicate satisfied", func(t *testing.T) {
|
||||
isPositive := Filter(func(x int) bool { return x > 0 })
|
||||
isPositive := Filter(N.MoreThan(0))
|
||||
// Should keep value when predicate is satisfied
|
||||
AssertEq(Some(5))(isPositive(Some(5)))(t)
|
||||
})
|
||||
|
||||
t.Run("negative case - predicate not satisfied", func(t *testing.T) {
|
||||
isPositive := Filter(func(x int) bool { return x > 0 })
|
||||
isPositive := Filter(N.MoreThan(0))
|
||||
// Should return None when predicate fails
|
||||
AssertEq(None[int]())(isPositive(Some(-1)))(t)
|
||||
AssertEq(None[int]())(isPositive(Some(0)))(t)
|
||||
})
|
||||
|
||||
t.Run("negative case - input is None", func(t *testing.T) {
|
||||
isPositive := Filter(func(x int) bool { return x > 0 })
|
||||
isPositive := Filter(N.MoreThan(0))
|
||||
// Should return None when input is None
|
||||
AssertEq(None[int]())(isPositive(None[int]()))(t)
|
||||
})
|
||||
|
||||
@@ -47,7 +47,7 @@ import (
|
||||
// eqBool := eq.FromStrictEquals[bool]()
|
||||
//
|
||||
// ab := strconv.Itoa
|
||||
// bc := func(s string) bool { return len(s) > 0 }
|
||||
// bc := S.IsNonEmpty
|
||||
//
|
||||
// assert := AssertLaws(t, eqInt, eqString, eqBool, ab, bc)
|
||||
// assert(42) // verifies laws hold for value 42
|
||||
|
||||
51
v2/idiomatic/readerioresult/filter.go
Normal file
51
v2/idiomatic/readerioresult/filter.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioresult
|
||||
|
||||
import "github.com/IBM/fp-go/v2/either"
|
||||
|
||||
// FilterOrElse filters a ReaderIOResult value based on a predicate in an idiomatic style.
|
||||
// If the ReaderIOResult computation succeeds and the predicate returns true, returns the original success value.
|
||||
// If the ReaderIOResult computation succeeds and the predicate returns false, returns an error with the error from onFalse.
|
||||
// If the ReaderIOResult computation fails, returns the original error without applying the predicate.
|
||||
//
|
||||
// This is the idiomatic version that returns an Operator for use in method chaining.
|
||||
// It's useful for adding validation to successful IO computations with dependencies, converting them to errors if they don't meet certain criteria.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// RIO "github.com/IBM/fp-go/v2/idiomatic/readerioresult"
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// )
|
||||
//
|
||||
// type Config struct {
|
||||
// MaxValue int
|
||||
// }
|
||||
//
|
||||
// isPositive := N.MoreThan(0)
|
||||
// onNegative := func(x int) error { return fmt.Errorf("%d is not positive", x) }
|
||||
//
|
||||
// result := RIO.Of[Config](5).
|
||||
// Pipe(RIO.FilterOrElse(isPositive, onNegative))(Config{MaxValue: 10})() // Ok(5)
|
||||
//
|
||||
// result2 := RIO.Of[Config](-3).
|
||||
// Pipe(RIO.FilterOrElse(isPositive, onNegative))(Config{MaxValue: 10})() // Error(error: "-3 is not positive")
|
||||
//
|
||||
//go:inline
|
||||
func FilterOrElse[R, A any](pred Predicate[A], onFalse func(A) error) Operator[R, A, A] {
|
||||
return ChainEitherK[R](either.FromPredicate(pred, onFalse))
|
||||
}
|
||||
@@ -16,9 +16,11 @@
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
"github.com/IBM/fp-go/v2/internal/chain"
|
||||
"github.com/IBM/fp-go/v2/internal/fromeither"
|
||||
"github.com/IBM/fp-go/v2/internal/fromio"
|
||||
"github.com/IBM/fp-go/v2/internal/fromreader"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
@@ -265,67 +267,67 @@ func MonadTap[R, A, B any](fa ReaderIOResult[R, A], f Kleisli[R, A, B]) ReaderIO
|
||||
return MonadChainFirst(fa, f)
|
||||
}
|
||||
|
||||
// // MonadChainEitherK chains a computation that returns an Either into a ReaderIOResult.
|
||||
// // The Either is automatically lifted into the ReaderIOResult context.
|
||||
// //
|
||||
// //go:inline
|
||||
// func MonadChainEitherK[R, A, B any](ma ReaderIOResult[R, A], f either.Kleisli[A, B]) ReaderIOResult[R, B] {
|
||||
// return fromeither.MonadChainEitherK(
|
||||
// MonadChain[R, A, B],
|
||||
// FromEither[R, B],
|
||||
// ma,
|
||||
// f,
|
||||
// )
|
||||
// }
|
||||
// MonadChainEitherK chains a computation that returns an Either into a ReaderIOResult.
|
||||
// The Either is automatically lifted into the ReaderIOResult context.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainEitherK[R, A, B any](ma ReaderIOResult[R, A], f either.Kleisli[error, A, B]) ReaderIOResult[R, B] {
|
||||
return fromeither.MonadChainEitherK(
|
||||
MonadChain[R, A, B],
|
||||
FromEither[R, B],
|
||||
ma,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// // ChainEitherK returns a function that chains an Either-returning function into ReaderIOResult.
|
||||
// // This is the curried version of MonadChainEitherK.
|
||||
// //
|
||||
// //go:inline
|
||||
// func ChainEitherK[R, A, B any](f either.Kleisli[A, B]) Operator[R, A, B] {
|
||||
// return fromeither.ChainEitherK(
|
||||
// Chain[R, A, B],
|
||||
// FromEither[R, B],
|
||||
// f,
|
||||
// )
|
||||
// }
|
||||
// ChainEitherK returns a function that chains an Either-returning function into ReaderIOResult.
|
||||
// This is the curried version of MonadChainEitherK.
|
||||
//
|
||||
//go:inline
|
||||
func ChainEitherK[R, A, B any](f either.Kleisli[error, A, B]) Operator[R, A, B] {
|
||||
return fromeither.ChainEitherK(
|
||||
Chain[R, A, B],
|
||||
FromEither[R, B],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// // MonadChainFirstEitherK chains an Either-returning computation but keeps the original value.
|
||||
// // Useful for validation or side effects that return Either.
|
||||
// //
|
||||
// //go:inline
|
||||
// func MonadChainFirstEitherK[R, A, B any](ma ReaderIOResult[R, A], f either.Kleisli[A, B]) ReaderIOResult[R, A] {
|
||||
// return fromeither.MonadChainFirstEitherK(
|
||||
// MonadChain[R, A, A],
|
||||
// MonadMap[R, B, A],
|
||||
// FromEither[R, B],
|
||||
// ma,
|
||||
// f,
|
||||
// )
|
||||
// }
|
||||
// MonadChainFirstEitherK chains an Either-returning computation but keeps the original value.
|
||||
// Useful for validation or side effects that return Either.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstEitherK[R, A, B any](ma ReaderIOResult[R, A], f either.Kleisli[error, A, B]) ReaderIOResult[R, A] {
|
||||
return fromeither.MonadChainFirstEitherK(
|
||||
MonadChain[R, A, A],
|
||||
MonadMap[R, B, A],
|
||||
FromEither[R, B],
|
||||
ma,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// //go:inline
|
||||
// func MonadTapEitherK[R, A, B any](ma ReaderIOResult[R, A], f either.Kleisli[A, B]) ReaderIOResult[R, A] {
|
||||
// return MonadChainFirstEitherK(ma, f)
|
||||
// }
|
||||
//go:inline
|
||||
func MonadTapEitherK[R, A, B any](ma ReaderIOResult[R, A], f either.Kleisli[error, A, B]) ReaderIOResult[R, A] {
|
||||
return MonadChainFirstEitherK(ma, f)
|
||||
}
|
||||
|
||||
// // ChainFirstEitherK returns a function that chains an Either computation while preserving the original value.
|
||||
// // This is the curried version of MonadChainFirstEitherK.
|
||||
// //
|
||||
// //go:inline
|
||||
// func ChainFirstEitherK[R, A, B any](f either.Kleisli[A, B]) Operator[R, A, A] {
|
||||
// return fromeither.ChainFirstEitherK(
|
||||
// Chain[R, A, A],
|
||||
// Map[R, B, A],
|
||||
// FromEither[R, B],
|
||||
// f,
|
||||
// )
|
||||
// }
|
||||
// ChainFirstEitherK returns a function that chains an Either computation while preserving the original value.
|
||||
// This is the curried version of MonadChainFirstEitherK.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstEitherK[R, A, B any](f either.Kleisli[error, A, B]) Operator[R, A, A] {
|
||||
return fromeither.ChainFirstEitherK(
|
||||
Chain[R, A, A],
|
||||
Map[R, B, A],
|
||||
FromEither[R, B],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// //go:inline
|
||||
// func TapEitherK[R, A, B any](f either.Kleisli[A, B]) Operator[R, A, A] {
|
||||
// return ChainFirstEitherK[R](f)
|
||||
// }
|
||||
//go:inline
|
||||
func TapEitherK[R, A, B any](f either.Kleisli[error, A, B]) Operator[R, A, A] {
|
||||
return ChainFirstEitherK[R](f)
|
||||
}
|
||||
|
||||
// MonadChainReaderK chains a Reader-returning computation into a ReaderIOResult.
|
||||
// The Reader is automatically lifted into the ReaderIOResult context.
|
||||
@@ -698,13 +700,15 @@ func Flatten[R, A any](mma ReaderIOResult[R, ReaderIOResult[R, A]]) ReaderIOResu
|
||||
return MonadChain(mma, function.Identity[ReaderIOResult[R, A]])
|
||||
}
|
||||
|
||||
// // FromEither lifts an Either into a ReaderIOResult context.
|
||||
// // The Either value is independent of any context or IO effects.
|
||||
// //
|
||||
// //go:inline
|
||||
// func FromEither[R, A any](t either.Either[A]) ReaderIOResult[R, A] {
|
||||
// return readerio.Of[R](t)
|
||||
// }
|
||||
// FromEither lifts an Either into a ReaderIOResult context.
|
||||
// The Either value is independent of any context or IO effects.
|
||||
func FromEither[R, A any](t either.Either[error, A]) ReaderIOResult[R, A] {
|
||||
return func(r R) IOResult[A] {
|
||||
return func() (A, error) {
|
||||
return either.Unwrap(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RightReader lifts a Reader into a ReaderIOResult, placing the result in the Right side.
|
||||
func RightReader[R, A any](ma Reader[R, A]) ReaderIOResult[R, A] {
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
@@ -54,6 +55,7 @@ type (
|
||||
// It is equivalent to Reader[R, IOResult[A]] or func(R) func() (A, error).
|
||||
ReaderIOResult[R, A any] = Reader[R, IOResult[A]]
|
||||
|
||||
// ReaderIO represents a computation that depends on an environment R and performs side effects.
|
||||
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
|
||||
|
||||
// Monoid represents a monoid structure for ReaderIOResult values.
|
||||
@@ -66,4 +68,6 @@ type (
|
||||
// Operator represents a transformation from ReaderIOResult[R, A] to ReaderIOResult[R, B].
|
||||
// It is commonly used in function composition pipelines.
|
||||
Operator[R, A, B any] = Kleisli[R, ReaderIOResult[R, A], B]
|
||||
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
)
|
||||
|
||||
51
v2/idiomatic/readerresult/filter.go
Normal file
51
v2/idiomatic/readerresult/filter.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerresult
|
||||
|
||||
import "github.com/IBM/fp-go/v2/either"
|
||||
|
||||
// FilterOrElse filters a ReaderResult value based on a predicate in an idiomatic style.
|
||||
// If the ReaderResult computation succeeds and the predicate returns true, returns the original success value.
|
||||
// If the ReaderResult computation succeeds and the predicate returns false, returns an error with the error from onFalse.
|
||||
// If the ReaderResult computation fails, returns the original error without applying the predicate.
|
||||
//
|
||||
// This is the idiomatic version that returns an Operator for use in method chaining.
|
||||
// It's useful for adding validation to successful computations, converting them to errors if they don't meet certain criteria.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// RR "github.com/IBM/fp-go/v2/idiomatic/readerresult"
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// )
|
||||
//
|
||||
// type Config struct {
|
||||
// MaxValue int
|
||||
// }
|
||||
//
|
||||
// isPositive := N.MoreThan(0)
|
||||
// onNegative := func(x int) error { return fmt.Errorf("%d is not positive", x) }
|
||||
//
|
||||
// result := RR.Of[Config](5).
|
||||
// Pipe(RR.FilterOrElse(isPositive, onNegative))(Config{MaxValue: 10}) // Ok(5)
|
||||
//
|
||||
// result2 := RR.Of[Config](-3).
|
||||
// Pipe(RR.FilterOrElse(isPositive, onNegative))(Config{MaxValue: 10}) // Error(error: "-3 is not positive")
|
||||
//
|
||||
//go:inline
|
||||
func FilterOrElse[R, A any](pred Predicate[A], onFalse func(A) error) Operator[R, A, A] {
|
||||
return ChainEitherK[R](either.FromPredicate(pred, onFalse))
|
||||
}
|
||||
@@ -226,7 +226,7 @@ func Ap[B, R, A any](fa ReaderResult[R, A]) Operator[R, func(A) B, B] {
|
||||
// Example:
|
||||
//
|
||||
// isPositive := readerresult.FromPredicate[Config](
|
||||
// func(x int) bool { return x > 0 },
|
||||
// N.MoreThan(0),
|
||||
// func(x int) error { return fmt.Errorf("%d is not positive", x) },
|
||||
// )
|
||||
// result := isPositive(5) // Returns ReaderResult that succeeds with 5
|
||||
|
||||
@@ -181,7 +181,7 @@ func TestMonadAp(t *testing.T) {
|
||||
|
||||
func TestFromPredicate(t *testing.T) {
|
||||
isPositive := FromPredicate[MyContext](
|
||||
func(x int) bool { return x > 0 },
|
||||
N.MoreThan(0),
|
||||
func(x int) error { return fmt.Errorf("%d is not positive", x) },
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
@@ -59,4 +60,6 @@ type (
|
||||
// Operator represents a transformation from ReaderResult[R, A] to ReaderResult[R, B].
|
||||
// It is commonly used in function composition pipelines.
|
||||
Operator[R, A, B any] = Kleisli[R, ReaderResult[R, A], B]
|
||||
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
)
|
||||
|
||||
@@ -216,7 +216,7 @@ func Fold[A, B any](onLeft func(error) B, onRight func(A) B) func(A, error) B {
|
||||
// Example:
|
||||
//
|
||||
// isPositive := either.FromPredicate(
|
||||
// func(x int) bool { return x > 0 },
|
||||
// N.MoreThan(0),
|
||||
// func(x int) error { return errors.New("not positive") },
|
||||
// )
|
||||
// result := isPositive(42) // Right(42)
|
||||
|
||||
@@ -429,7 +429,7 @@ func BenchmarkReduce_Left(b *testing.B) {
|
||||
// Benchmark FromPredicate
|
||||
func BenchmarkFromPredicate_Pass(b *testing.B) {
|
||||
pred := FromPredicate(
|
||||
func(x int) bool { return x > 0 },
|
||||
N.MoreThan(0),
|
||||
func(x int) error { return errBench },
|
||||
)
|
||||
b.ResetTimer()
|
||||
@@ -441,7 +441,7 @@ func BenchmarkFromPredicate_Pass(b *testing.B) {
|
||||
|
||||
func BenchmarkFromPredicate_Fail(b *testing.B) {
|
||||
pred := FromPredicate(
|
||||
func(x int) bool { return x > 0 },
|
||||
N.MoreThan(0),
|
||||
func(x int) error { return errBench },
|
||||
)
|
||||
b.ResetTimer()
|
||||
|
||||
45
v2/idiomatic/result/filter.go
Normal file
45
v2/idiomatic/result/filter.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package result
|
||||
|
||||
// FilterOrElse filters a Result value based on a predicate in an idiomatic style.
|
||||
// If the Result is Ok and the predicate returns true, returns the original Ok.
|
||||
// If the Result is Ok and the predicate returns false, returns Error with the error from onFalse.
|
||||
// If the Result is Error, returns the original Error without applying the predicate.
|
||||
//
|
||||
// This is the idiomatic version that returns an Operator for use in method chaining.
|
||||
// It's useful for adding validation to successful results, converting them to errors if they don't meet certain criteria.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// R "github.com/IBM/fp-go/v2/idiomatic/result"
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// )
|
||||
//
|
||||
// isPositive := N.MoreThan(0)
|
||||
// onNegative := func(x int) error { return fmt.Errorf("%d is not positive", x) }
|
||||
//
|
||||
// result := R.Of(5).
|
||||
// Pipe(R.FilterOrElse(isPositive, onNegative)) // Ok(5)
|
||||
//
|
||||
// result2 := R.Of(-3).
|
||||
// Pipe(R.FilterOrElse(isPositive, onNegative)) // Error(error: "-3 is not positive")
|
||||
//
|
||||
//go:inline
|
||||
func FilterOrElse[A any](pred Predicate[A], onFalse func(A) error) Operator[A, A] {
|
||||
return Chain(FromPredicate(pred, onFalse))
|
||||
}
|
||||
160
v2/idiomatic/result/filter_test.go
Normal file
160
v2/idiomatic/result/filter_test.go
Normal file
@@ -0,0 +1,160 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package result
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFilterOrElse(t *testing.T) {
|
||||
// Test with positive predicate
|
||||
isPositive := N.MoreThan(0)
|
||||
onNegative := func(x int) error { return fmt.Errorf("%d is not positive", x) }
|
||||
|
||||
// Test value that passes predicate
|
||||
AssertEq(Right(5))(Pipe2(5, Of, FilterOrElse(isPositive, onNegative)))(t)
|
||||
|
||||
// Test value that fails predicate
|
||||
_, err := Pipe2(-3, Of, FilterOrElse(isPositive, onNegative))
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "-3 is not positive", err.Error())
|
||||
|
||||
// Test value at boundary (zero)
|
||||
_, err = Pipe2(0, Of, FilterOrElse(isPositive, onNegative))
|
||||
assert.Error(t, err)
|
||||
|
||||
// Test error value (should pass through unchanged)
|
||||
originalError := errors.New("original error")
|
||||
_, err = Pipe2(originalError, Left[int], FilterOrElse(isPositive, onNegative))
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, originalError, err)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_StringValidation(t *testing.T) {
|
||||
// Test with string length validation
|
||||
isNotEmpty := func(s string) bool { return len(s) > 0 }
|
||||
onEmpty := func(s string) error { return errors.New("string is empty") }
|
||||
|
||||
// Test non-empty string
|
||||
AssertEq(Right("hello"))(Pipe2("hello", Of, FilterOrElse(isNotEmpty, onEmpty)))(t)
|
||||
|
||||
// Test empty string
|
||||
_, err := Pipe2("", Of, FilterOrElse(isNotEmpty, onEmpty))
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "string is empty", err.Error())
|
||||
|
||||
// Test error value
|
||||
originalError := errors.New("validation error")
|
||||
_, err = Pipe2(originalError, Left[string], FilterOrElse(isNotEmpty, onEmpty))
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, originalError, err)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_ComplexPredicate(t *testing.T) {
|
||||
// Test with range validation
|
||||
inRange := func(x int) bool { return x >= 10 && x <= 100 }
|
||||
outOfRange := func(x int) error { return fmt.Errorf("%d is out of range [10, 100]", x) }
|
||||
|
||||
// Test value in range
|
||||
AssertEq(Right(50))(Pipe2(50, Of, FilterOrElse(inRange, outOfRange)))(t)
|
||||
|
||||
// Test value below range
|
||||
_, err := Pipe2(5, Of, FilterOrElse(inRange, outOfRange))
|
||||
assert.Error(t, err)
|
||||
|
||||
// Test value above range
|
||||
_, err = Pipe2(150, Of, FilterOrElse(inRange, outOfRange))
|
||||
assert.Error(t, err)
|
||||
|
||||
// Test boundary values
|
||||
AssertEq(Right(10))(Pipe2(10, Of, FilterOrElse(inRange, outOfRange)))(t)
|
||||
AssertEq(Right(100))(Pipe2(100, Of, FilterOrElse(inRange, outOfRange)))(t)
|
||||
}
|
||||
|
||||
func TestFilterOrElse_ChainedFilters(t *testing.T) {
|
||||
// Test chaining multiple filters
|
||||
isPositive := N.MoreThan(0)
|
||||
onNegative := func(x int) error { return fmt.Errorf("%d is not positive", x) }
|
||||
|
||||
isEven := func(x int) bool { return x%2 == 0 }
|
||||
onOdd := func(x int) error { return fmt.Errorf("%d is not even", x) }
|
||||
|
||||
// Test value that passes both filters
|
||||
AssertEq(Right(4))(Pipe3(4, Of, FilterOrElse(isPositive, onNegative), FilterOrElse(isEven, onOdd)))(t)
|
||||
|
||||
// Test value that fails first filter
|
||||
_, err := Pipe3(-2, Of, FilterOrElse(isPositive, onNegative), FilterOrElse(isEven, onOdd))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not positive")
|
||||
|
||||
// Test value that passes first but fails second filter
|
||||
_, err = Pipe3(3, Of, FilterOrElse(isPositive, onNegative), FilterOrElse(isEven, onOdd))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not even")
|
||||
}
|
||||
|
||||
func TestFilterOrElse_WithStructs(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
// Test with struct validation
|
||||
isAdult := func(u User) bool { return u.Age >= 18 }
|
||||
onMinor := func(u User) error { return fmt.Errorf("%s is not an adult (age: %d)", u.Name, u.Age) }
|
||||
|
||||
// Test adult user
|
||||
adult := User{Name: "Alice", Age: 25}
|
||||
AssertEq(Right(adult))(Pipe2(adult, Of, FilterOrElse(isAdult, onMinor)))(t)
|
||||
|
||||
// Test minor user
|
||||
minor := User{Name: "Bob", Age: 16}
|
||||
_, err := Pipe2(minor, Of, FilterOrElse(isAdult, onMinor))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Bob is not an adult")
|
||||
}
|
||||
|
||||
func TestFilterOrElse_WithChain(t *testing.T) {
|
||||
// Test FilterOrElse in a chain with other operations
|
||||
isPositive := N.MoreThan(0)
|
||||
onNegative := func(x int) error { return fmt.Errorf("%d is not positive", x) }
|
||||
|
||||
double := func(x int) (int, error) { return x * 2, nil }
|
||||
|
||||
// Test successful chain
|
||||
AssertEq(Right(10))(Pipe3(5, Of, FilterOrElse(isPositive, onNegative), Chain(double)))(t)
|
||||
|
||||
// Test chain with filter failure
|
||||
_, err := Pipe3(-5, Of, FilterOrElse(isPositive, onNegative), Chain(double))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not positive")
|
||||
}
|
||||
|
||||
func TestFilterOrElse_ErrorMessages(t *testing.T) {
|
||||
// Test that error messages are properly propagated
|
||||
isPositive := N.MoreThan(0)
|
||||
onNegative := func(x int) error { return fmt.Errorf("value %d is not positive", x) }
|
||||
|
||||
result, err := Pipe2(-5, Of, FilterOrElse(isPositive, onNegative))
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "value -5 is not positive", err.Error())
|
||||
assert.Equal(t, 0, result) // default value for int
|
||||
}
|
||||
@@ -107,7 +107,7 @@ func TestReduce(t *testing.T) {
|
||||
// TestFromPredicate tests creating Result from a predicate
|
||||
func TestFromPredicate(t *testing.T) {
|
||||
isPositive := FromPredicate(
|
||||
func(x int) bool { return x > 0 },
|
||||
N.MoreThan(0),
|
||||
func(x int) error { return fmt.Errorf("%d is not positive", x) },
|
||||
)
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ import (
|
||||
// eqError := eq.FromStrictEquals[error]()
|
||||
//
|
||||
// ab := strconv.Itoa
|
||||
// bc := func(s string) bool { return len(s) > 0 }
|
||||
// bc := S.IsNonEmpty
|
||||
//
|
||||
// testing.AssertLaws(t, eqError, eqInt, eqString, eq.FromStrictEquals[bool](), ab, bc)(42)
|
||||
// }
|
||||
|
||||
@@ -19,15 +19,27 @@ import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
)
|
||||
|
||||
// Option is a type alias for option.Option, provided for convenience
|
||||
// when working with Either and Option together.
|
||||
type (
|
||||
Option[A any] = option.Option[A]
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
// Option is a type alias for option.Option, provided for convenience
|
||||
// when working with Result and Option together.
|
||||
Option[A any] = option.Option[A]
|
||||
|
||||
// Lens is an optic that focuses on a field of type T within a structure of type S.
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
|
||||
// Endomorphism represents a function from a type to itself (T -> T).
|
||||
Endomorphism[T any] = endomorphism.Endomorphism[T]
|
||||
|
||||
Kleisli[A, B any] = func(A) (B, error)
|
||||
// Kleisli represents a Kleisli arrow for the idiomatic Result pattern.
|
||||
// It's a function from A to (B, error), following Go's idiomatic error handling.
|
||||
Kleisli[A, B any] = func(A) (B, error)
|
||||
|
||||
// Operator represents a function that transforms one Result into another.
|
||||
// It takes (A, error) and produces (B, error), following Go's idiomatic pattern.
|
||||
Operator[A, B any] = func(A, error) (B, error)
|
||||
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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
144
v2/io/rec.go
Normal 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
462
v2/io/rec_test.go
Normal 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)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
133
v2/ioeither/ap_test.go
Normal file
@@ -0,0 +1,133 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ioeither
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMonadApFirstExtended(t *testing.T) {
|
||||
t.Run("both Right values - returns first", func(t *testing.T) {
|
||||
first := Of[error]("first")
|
||||
second := Of[error]("second")
|
||||
result := MonadApFirst(first, second)
|
||||
assert.Equal(t, E.Of[error]("first"), result())
|
||||
})
|
||||
|
||||
t.Run("first Left - returns Left", func(t *testing.T) {
|
||||
first := Left[string](errors.New("error1"))
|
||||
second := Of[error]("second")
|
||||
result := MonadApFirst(first, second)
|
||||
assert.True(t, E.IsLeft(result()))
|
||||
})
|
||||
|
||||
t.Run("second Left - returns Left", func(t *testing.T) {
|
||||
first := Of[error]("first")
|
||||
second := Left[string](errors.New("error2"))
|
||||
result := MonadApFirst(first, second)
|
||||
assert.True(t, E.IsLeft(result()))
|
||||
})
|
||||
|
||||
t.Run("both Left - returns first Left", func(t *testing.T) {
|
||||
first := Left[string](errors.New("error1"))
|
||||
second := Left[string](errors.New("error2"))
|
||||
result := MonadApFirst(first, second)
|
||||
assert.True(t, E.IsLeft(result()))
|
||||
})
|
||||
}
|
||||
|
||||
func TestApFirstExtended(t *testing.T) {
|
||||
t.Run("composition with Map", func(t *testing.T) {
|
||||
result := F.Pipe2(
|
||||
Of[error](10),
|
||||
ApFirst[int](Of[error](20)),
|
||||
Map[error](func(x int) int { return x * 2 }),
|
||||
)
|
||||
assert.Equal(t, E.Of[error](20), result())
|
||||
})
|
||||
|
||||
t.Run("with different types", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of[error]("text"),
|
||||
ApFirst[string](Of[error](42)),
|
||||
)
|
||||
assert.Equal(t, E.Of[error]("text"), result())
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadApSecondExtended(t *testing.T) {
|
||||
t.Run("both Right values - returns second", func(t *testing.T) {
|
||||
first := Of[error]("first")
|
||||
second := Of[error]("second")
|
||||
result := MonadApSecond(first, second)
|
||||
assert.Equal(t, E.Of[error]("second"), result())
|
||||
})
|
||||
|
||||
t.Run("first Left - returns Left", func(t *testing.T) {
|
||||
first := Left[string](errors.New("error1"))
|
||||
second := Of[error]("second")
|
||||
result := MonadApSecond(first, second)
|
||||
assert.True(t, E.IsLeft(result()))
|
||||
})
|
||||
|
||||
t.Run("second Left - returns Left", func(t *testing.T) {
|
||||
first := Of[error]("first")
|
||||
second := Left[string](errors.New("error2"))
|
||||
result := MonadApSecond(first, second)
|
||||
assert.True(t, E.IsLeft(result()))
|
||||
})
|
||||
|
||||
t.Run("both Left - returns first Left", func(t *testing.T) {
|
||||
first := Left[string](errors.New("error1"))
|
||||
second := Left[string](errors.New("error2"))
|
||||
result := MonadApSecond(first, second)
|
||||
assert.True(t, E.IsLeft(result()))
|
||||
})
|
||||
}
|
||||
|
||||
func TestApSecondExtended(t *testing.T) {
|
||||
t.Run("composition with Map", func(t *testing.T) {
|
||||
result := F.Pipe2(
|
||||
Of[error](10),
|
||||
ApSecond[int](Of[error](20)),
|
||||
Map[error](func(x int) int { return x * 2 }),
|
||||
)
|
||||
assert.Equal(t, E.Of[error](40), result())
|
||||
})
|
||||
|
||||
t.Run("sequence of operations", func(t *testing.T) {
|
||||
result := F.Pipe3(
|
||||
Of[error](1),
|
||||
ApSecond[int](Of[error](2)),
|
||||
ApSecond[int](Of[error](3)),
|
||||
ApSecond[int](Of[error](4)),
|
||||
)
|
||||
assert.Equal(t, E.Of[error](4), result())
|
||||
})
|
||||
|
||||
t.Run("with different types", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of[error]("text"),
|
||||
ApSecond[string](Of[error](42)),
|
||||
)
|
||||
assert.Equal(t, E.Of[error](42), result())
|
||||
})
|
||||
}
|
||||
158
v2/ioeither/bracket_test.go
Normal file
158
v2/ioeither/bracket_test.go
Normal file
@@ -0,0 +1,158 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ioeither
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBracket(t *testing.T) {
|
||||
t.Run("successful acquisition, use, and release", func(t *testing.T) {
|
||||
acquired := false
|
||||
used := false
|
||||
released := false
|
||||
|
||||
acquire := func() IOEither[error, string] {
|
||||
return func() E.Either[error, string] {
|
||||
acquired = true
|
||||
return E.Right[error]("resource")
|
||||
}
|
||||
}()
|
||||
|
||||
use := func(r string) IOEither[error, int] {
|
||||
return func() E.Either[error, int] {
|
||||
used = true
|
||||
assert.Equal(t, "resource", r)
|
||||
return E.Right[error](42)
|
||||
}
|
||||
}
|
||||
|
||||
release := func(r string, result E.Either[error, int]) IOEither[error, any] {
|
||||
return func() E.Either[error, any] {
|
||||
released = true
|
||||
assert.Equal(t, "resource", r)
|
||||
assert.True(t, E.IsRight(result))
|
||||
return E.Right[error, any](nil)
|
||||
}
|
||||
}
|
||||
|
||||
result := Bracket(acquire, use, release)()
|
||||
|
||||
assert.True(t, acquired)
|
||||
assert.True(t, used)
|
||||
assert.True(t, released)
|
||||
assert.Equal(t, E.Right[error](42), result)
|
||||
})
|
||||
|
||||
t.Run("acquisition fails", func(t *testing.T) {
|
||||
used := false
|
||||
released := false
|
||||
|
||||
acquire := Left[string](errors.New("acquisition failed"))
|
||||
|
||||
use := func(r string) IOEither[error, int] {
|
||||
used = true
|
||||
return Of[error](42)
|
||||
}
|
||||
|
||||
release := func(r string, result E.Either[error, int]) IOEither[error, any] {
|
||||
released = true
|
||||
return Of[error, any](nil)
|
||||
}
|
||||
|
||||
result := Bracket(acquire, use, release)()
|
||||
|
||||
assert.False(t, used)
|
||||
assert.False(t, released)
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("use fails but release is called", func(t *testing.T) {
|
||||
acquired := false
|
||||
released := false
|
||||
var releaseResult E.Either[error, int]
|
||||
|
||||
acquire := func() IOEither[error, string] {
|
||||
return func() E.Either[error, string] {
|
||||
acquired = true
|
||||
return E.Right[error]("resource")
|
||||
}
|
||||
}()
|
||||
|
||||
use := func(r string) IOEither[error, int] {
|
||||
return Left[int](errors.New("use failed"))
|
||||
}
|
||||
|
||||
release := func(r string, result E.Either[error, int]) IOEither[error, any] {
|
||||
return func() E.Either[error, any] {
|
||||
released = true
|
||||
releaseResult = result
|
||||
assert.Equal(t, "resource", r)
|
||||
return E.Right[error, any](nil)
|
||||
}
|
||||
}
|
||||
|
||||
result := Bracket(acquire, use, release)()
|
||||
|
||||
assert.True(t, acquired)
|
||||
assert.True(t, released)
|
||||
assert.True(t, E.IsLeft(result))
|
||||
assert.True(t, E.IsLeft(releaseResult))
|
||||
})
|
||||
|
||||
t.Run("release is called even when use succeeds", func(t *testing.T) {
|
||||
releaseCallCount := 0
|
||||
|
||||
acquire := Of[error]("resource")
|
||||
|
||||
use := func(r string) IOEither[error, int] {
|
||||
return Of[error](100)
|
||||
}
|
||||
|
||||
release := func(r string, result E.Either[error, int]) IOEither[error, any] {
|
||||
return func() E.Either[error, any] {
|
||||
releaseCallCount++
|
||||
return E.Right[error, any](nil)
|
||||
}
|
||||
}
|
||||
|
||||
result := Bracket(acquire, use, release)()
|
||||
|
||||
assert.Equal(t, 1, releaseCallCount)
|
||||
assert.Equal(t, E.Right[error](100), result)
|
||||
})
|
||||
|
||||
t.Run("release error overrides successful result", func(t *testing.T) {
|
||||
acquire := Of[error]("resource")
|
||||
|
||||
use := func(r string) IOEither[error, int] {
|
||||
return Of[error](42)
|
||||
}
|
||||
|
||||
release := func(r string, result E.Either[error, int]) IOEither[error, any] {
|
||||
return Left[any](errors.New("release failed"))
|
||||
}
|
||||
|
||||
result := Bracket(acquire, use, release)()
|
||||
|
||||
// According to bracket semantics, release errors are propagated
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
}
|
||||
78
v2/ioeither/file/copy.go
Normal file
78
v2/ioeither/file/copy.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
)
|
||||
|
||||
// CopyFile copies a file from source to destination path with proper resource management.
|
||||
//
|
||||
// This is a curried function that follows the "data-last" pattern, where the source path
|
||||
// is provided first, returning a function that accepts the destination path. This design
|
||||
// enables partial application and better composition with other functional operations.
|
||||
//
|
||||
// The function uses [ioeither.WithResource] to ensure both source and destination files
|
||||
// are properly closed, even if an error occurs during the copy operation. The copy is
|
||||
// performed using [io.Copy] which efficiently transfers data between the files.
|
||||
//
|
||||
// Parameters:
|
||||
// - src: The path to the source file to copy from
|
||||
//
|
||||
// Returns:
|
||||
// - A function that accepts the destination path and returns an [IOEither] that:
|
||||
// - On success: Contains the destination path (Right)
|
||||
// - On failure: Contains the error (Left) from opening, copying, or closing files
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a copy operation for a specific source file
|
||||
// copyFromSource := CopyFile("/path/to/source.txt")
|
||||
//
|
||||
// // Execute the copy to a destination
|
||||
// result := copyFromSource("/path/to/destination.txt")()
|
||||
//
|
||||
// // Or use it in a pipeline
|
||||
// result := F.Pipe1(
|
||||
// CopyFile("/path/to/source.txt"),
|
||||
// ioeither.Map(func(dst string) string {
|
||||
// return "Copied to: " + dst
|
||||
// }),
|
||||
// )("/path/to/destination.txt")()
|
||||
//
|
||||
//go:inline
|
||||
func CopyFile(src string) func(dst string) IOEither[error, string] {
|
||||
withSrc := ioeither.WithResource[int64](Open(src), Close)
|
||||
return func(dst string) IOEither[error, string] {
|
||||
withDst := ioeither.WithResource[int64](Create(dst), Close)
|
||||
|
||||
return F.Pipe1(
|
||||
withSrc(func(srcFile *os.File) IOEither[error, int64] {
|
||||
return withDst(func(dstFile *os.File) IOEither[error, int64] {
|
||||
return func() Either[error, int64] {
|
||||
return either.TryCatchError(io.Copy(dstFile, srcFile))
|
||||
}
|
||||
})
|
||||
}),
|
||||
ioeither.MapTo[error, int64](dst),
|
||||
)
|
||||
}
|
||||
}
|
||||
360
v2/ioeither/file/copy_test.go
Normal file
360
v2/ioeither/file/copy_test.go
Normal file
@@ -0,0 +1,360 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
IOE "github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestCopyFileSuccess tests successful file copying
|
||||
func TestCopyFileSuccess(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create source file with test content
|
||||
srcPath := filepath.Join(tempDir, "source.txt")
|
||||
testContent := []byte("Hello, CopyFile! This is test content.")
|
||||
err := os.WriteFile(srcPath, testContent, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Copy to destination
|
||||
dstPath := filepath.Join(tempDir, "destination.txt")
|
||||
result := CopyFile(srcPath)(dstPath)()
|
||||
|
||||
// Verify success
|
||||
assert.True(t, E.IsRight(result))
|
||||
returnedPath := E.GetOrElse(func(error) string { return "" })(result)
|
||||
assert.Equal(t, dstPath, returnedPath)
|
||||
|
||||
// Verify destination file content matches source
|
||||
dstContent, err := os.ReadFile(dstPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testContent, dstContent)
|
||||
}
|
||||
|
||||
// TestCopyFileEmptyFile tests copying an empty file
|
||||
func TestCopyFileEmptyFile(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create empty source file
|
||||
srcPath := filepath.Join(tempDir, "empty_source.txt")
|
||||
err := os.WriteFile(srcPath, []byte{}, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Copy to destination
|
||||
dstPath := filepath.Join(tempDir, "empty_destination.txt")
|
||||
result := CopyFile(srcPath)(dstPath)()
|
||||
|
||||
// Verify success
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify destination file is also empty
|
||||
dstContent, err := os.ReadFile(dstPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, len(dstContent))
|
||||
}
|
||||
|
||||
// TestCopyFileLargeFile tests copying a larger file
|
||||
func TestCopyFileLargeFile(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create source file with larger content (1MB)
|
||||
srcPath := filepath.Join(tempDir, "large_source.txt")
|
||||
largeContent := make([]byte, 1024*1024) // 1MB
|
||||
for i := range largeContent {
|
||||
largeContent[i] = byte(i % 256)
|
||||
}
|
||||
err := os.WriteFile(srcPath, largeContent, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Copy to destination
|
||||
dstPath := filepath.Join(tempDir, "large_destination.txt")
|
||||
result := CopyFile(srcPath)(dstPath)()
|
||||
|
||||
// Verify success
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify destination file content matches source
|
||||
dstContent, err := os.ReadFile(dstPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, largeContent, dstContent)
|
||||
}
|
||||
|
||||
// TestCopyFileSourceNotFound tests error when source file doesn't exist
|
||||
func TestCopyFileSourceNotFound(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
srcPath := filepath.Join(tempDir, "nonexistent_source.txt")
|
||||
dstPath := filepath.Join(tempDir, "destination.txt")
|
||||
|
||||
result := CopyFile(srcPath)(dstPath)()
|
||||
|
||||
// Verify failure
|
||||
assert.True(t, E.IsLeft(result))
|
||||
err := E.Fold(func(e error) error { return e }, func(string) error { return nil })(result)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// TestCopyFileDestinationDirectoryNotFound tests error when destination directory doesn't exist
|
||||
func TestCopyFileDestinationDirectoryNotFound(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create source file
|
||||
srcPath := filepath.Join(tempDir, "source.txt")
|
||||
err := os.WriteFile(srcPath, []byte("test"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try to copy to non-existent directory
|
||||
dstPath := filepath.Join(tempDir, "nonexistent_dir", "destination.txt")
|
||||
result := CopyFile(srcPath)(dstPath)()
|
||||
|
||||
// Verify failure
|
||||
assert.True(t, E.IsLeft(result))
|
||||
}
|
||||
|
||||
// TestCopyFileOverwriteExisting tests overwriting an existing destination file
|
||||
func TestCopyFileOverwriteExisting(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create source file
|
||||
srcPath := filepath.Join(tempDir, "source.txt")
|
||||
newContent := []byte("New content")
|
||||
err := os.WriteFile(srcPath, newContent, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create existing destination file with different content
|
||||
dstPath := filepath.Join(tempDir, "destination.txt")
|
||||
oldContent := []byte("Old content that should be replaced")
|
||||
err = os.WriteFile(dstPath, oldContent, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Copy and overwrite
|
||||
result := CopyFile(srcPath)(dstPath)()
|
||||
|
||||
// Verify success
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify destination has new content
|
||||
dstContent, err := os.ReadFile(dstPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, newContent, dstContent)
|
||||
}
|
||||
|
||||
// TestCopyFileCurrying tests the curried nature of CopyFile (data-last pattern)
|
||||
func TestCopyFileCurrying(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create source file
|
||||
srcPath := filepath.Join(tempDir, "source.txt")
|
||||
testContent := []byte("Currying test content")
|
||||
err := os.WriteFile(srcPath, testContent, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a partially applied function
|
||||
copyFromSource := CopyFile(srcPath)
|
||||
|
||||
// Use the partially applied function multiple times
|
||||
dst1 := filepath.Join(tempDir, "dest1.txt")
|
||||
dst2 := filepath.Join(tempDir, "dest2.txt")
|
||||
|
||||
result1 := copyFromSource(dst1)()
|
||||
result2 := copyFromSource(dst2)()
|
||||
|
||||
// Verify both copies succeeded
|
||||
assert.True(t, E.IsRight(result1))
|
||||
assert.True(t, E.IsRight(result2))
|
||||
|
||||
// Verify both destinations have the same content
|
||||
content1, err := os.ReadFile(dst1)
|
||||
require.NoError(t, err)
|
||||
content2, err := os.ReadFile(dst2)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testContent, content1)
|
||||
assert.Equal(t, testContent, content2)
|
||||
}
|
||||
|
||||
// TestCopyFileComposition tests composing CopyFile with other operations
|
||||
func TestCopyFileComposition(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create source file
|
||||
srcPath := filepath.Join(tempDir, "source.txt")
|
||||
testContent := []byte("Composition test")
|
||||
err := os.WriteFile(srcPath, testContent, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
dstPath := filepath.Join(tempDir, "destination.txt")
|
||||
|
||||
// Compose CopyFile with Map to transform the result
|
||||
result := F.Pipe1(
|
||||
CopyFile(srcPath)(dstPath),
|
||||
IOE.Map[error](func(dst string) string {
|
||||
return "Successfully copied to: " + dst
|
||||
}),
|
||||
)()
|
||||
|
||||
// Verify success and transformation
|
||||
assert.True(t, E.IsRight(result))
|
||||
message := E.GetOrElse(func(error) string { return "" })(result)
|
||||
assert.Equal(t, "Successfully copied to: "+dstPath, message)
|
||||
|
||||
// Verify file was actually copied
|
||||
dstContent, err := os.ReadFile(dstPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testContent, dstContent)
|
||||
}
|
||||
|
||||
// TestCopyFileChaining tests chaining multiple copy operations
|
||||
func TestCopyFileChaining(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create source file
|
||||
srcPath := filepath.Join(tempDir, "source.txt")
|
||||
testContent := []byte("Chaining test")
|
||||
err := os.WriteFile(srcPath, testContent, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
dst1Path := filepath.Join(tempDir, "dest1.txt")
|
||||
dst2Path := filepath.Join(tempDir, "dest2.txt")
|
||||
|
||||
// Chain two copy operations
|
||||
result := F.Pipe1(
|
||||
CopyFile(srcPath)(dst1Path),
|
||||
IOE.Chain[error](func(string) IOEither[error, string] {
|
||||
return CopyFile(dst1Path)(dst2Path)
|
||||
}),
|
||||
)()
|
||||
|
||||
// Verify success
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify both files exist with correct content
|
||||
content1, err := os.ReadFile(dst1Path)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testContent, content1)
|
||||
|
||||
content2, err := os.ReadFile(dst2Path)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testContent, content2)
|
||||
}
|
||||
|
||||
// TestCopyFileWithBinaryContent tests copying binary files
|
||||
func TestCopyFileWithBinaryContent(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create source file with binary content
|
||||
srcPath := filepath.Join(tempDir, "binary_source.bin")
|
||||
binaryContent := []byte{0x00, 0x01, 0x02, 0xFF, 0xFE, 0xFD, 0x7F, 0x80}
|
||||
err := os.WriteFile(srcPath, binaryContent, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Copy to destination
|
||||
dstPath := filepath.Join(tempDir, "binary_destination.bin")
|
||||
result := CopyFile(srcPath)(dstPath)()
|
||||
|
||||
// Verify success
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify binary content is preserved
|
||||
dstContent, err := os.ReadFile(dstPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, binaryContent, dstContent)
|
||||
}
|
||||
|
||||
// TestCopyFileErrorHandling tests error handling with Either operations
|
||||
func TestCopyFileErrorHandling(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
srcPath := filepath.Join(tempDir, "nonexistent.txt")
|
||||
dstPath := filepath.Join(tempDir, "destination.txt")
|
||||
|
||||
result := CopyFile(srcPath)(dstPath)()
|
||||
|
||||
// Test error handling with Fold
|
||||
message := E.Fold(
|
||||
func(err error) string { return "Error: " + err.Error() },
|
||||
func(dst string) string { return "Success: " + dst },
|
||||
)(result)
|
||||
|
||||
assert.Contains(t, message, "Error:")
|
||||
}
|
||||
|
||||
// TestCopyFileResourceCleanup tests that resources are properly cleaned up
|
||||
func TestCopyFileResourceCleanup(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create source file
|
||||
srcPath := filepath.Join(tempDir, "source.txt")
|
||||
testContent := []byte("Resource cleanup test")
|
||||
err := os.WriteFile(srcPath, testContent, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
dstPath := filepath.Join(tempDir, "destination.txt")
|
||||
|
||||
// Perform copy
|
||||
result := CopyFile(srcPath)(dstPath)()
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify we can immediately delete both files (no file handles left open)
|
||||
err = os.Remove(srcPath)
|
||||
assert.NoError(t, err, "Source file should be closed and deletable")
|
||||
|
||||
err = os.Remove(dstPath)
|
||||
assert.NoError(t, err, "Destination file should be closed and deletable")
|
||||
}
|
||||
|
||||
// TestCopyFileMultipleOperations tests using CopyFile multiple times independently
|
||||
func TestCopyFileMultipleOperations(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create multiple source files
|
||||
src1 := filepath.Join(tempDir, "source1.txt")
|
||||
src2 := filepath.Join(tempDir, "source2.txt")
|
||||
content1 := []byte("Content 1")
|
||||
content2 := []byte("Content 2")
|
||||
|
||||
err := os.WriteFile(src1, content1, 0644)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(src2, content2, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Perform multiple independent copies
|
||||
dst1 := filepath.Join(tempDir, "dest1.txt")
|
||||
dst2 := filepath.Join(tempDir, "dest2.txt")
|
||||
|
||||
result1 := CopyFile(src1)(dst1)()
|
||||
result2 := CopyFile(src2)(dst2)()
|
||||
|
||||
// Verify both succeeded
|
||||
assert.True(t, E.IsRight(result1))
|
||||
assert.True(t, E.IsRight(result2))
|
||||
|
||||
// Verify correct content in each destination
|
||||
dstContent1, err := os.ReadFile(dst1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, content1, dstContent1)
|
||||
|
||||
dstContent2, err := os.ReadFile(dst2)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, content2, dstContent2)
|
||||
}
|
||||
21
v2/ioeither/file/coverage.out
Normal file
21
v2/ioeither/file/coverage.out
Normal file
@@ -0,0 +1,21 @@
|
||||
mode: set
|
||||
github.com/IBM/fp-go/v2/ioeither/file/dir.go:40.70,41.55 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/dir.go:41.55,43.3 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/dir.go:62.67,63.55 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/dir.go:63.55,65.3 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/file.go:77.81,78.51 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/file.go:78.51,79.56 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/file.go:79.56,81.4 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/file.go:86.50,87.55 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/file.go:87.55,89.3 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/file.go:93.51,94.52 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/file.go:94.52,96.3 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/read.go:76.106,80.2 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/readall.go:41.83,49.2 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/tempfile.go:42.76,44.2 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/write.go:24.69,25.43 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/write.go:25.43,26.56 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/write.go:26.56,29.4 2 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/write.go:45.73,47.67 2 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/write.go:47.67,53.3 1 1
|
||||
github.com/IBM/fp-go/v2/ioeither/file/write.go:71.105,75.2 1 1
|
||||
@@ -21,14 +21,44 @@ import (
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
)
|
||||
|
||||
// MkdirAll create a sequence of directories, see [os.MkdirAll]
|
||||
// MkdirAll creates a directory and all necessary parent directories with the specified permissions.
|
||||
// If the directory already exists, MkdirAll does nothing and returns success.
|
||||
// This is equivalent to the Unix command `mkdir -p`.
|
||||
//
|
||||
// The perm parameter specifies the Unix permission bits for the created directories.
|
||||
// Common values include 0755 (rwxr-xr-x) for directories.
|
||||
//
|
||||
// Returns an IOEither that, when executed, creates the directory structure and returns
|
||||
// the path on success or an error on failure.
|
||||
//
|
||||
// See [os.MkdirAll] for more details.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// mkdirOp := MkdirAll("/tmp/my/nested/dir", 0755)
|
||||
// result := mkdirOp() // Either[error, string]
|
||||
func MkdirAll(path string, perm os.FileMode) IOEither[error, string] {
|
||||
return ioeither.TryCatchError(func() (string, error) {
|
||||
return path, os.MkdirAll(path, perm)
|
||||
})
|
||||
}
|
||||
|
||||
// Mkdir create a directory, see [os.Mkdir]
|
||||
// Mkdir creates a single directory with the specified permissions.
|
||||
// Unlike MkdirAll, it returns an error if the parent directory does not exist
|
||||
// or if the directory already exists.
|
||||
//
|
||||
// The perm parameter specifies the Unix permission bits for the created directory.
|
||||
// Common values include 0755 (rwxr-xr-x) for directories.
|
||||
//
|
||||
// Returns an IOEither that, when executed, creates the directory and returns
|
||||
// the path on success or an error on failure.
|
||||
//
|
||||
// See [os.Mkdir] for more details.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// mkdirOp := Mkdir("/tmp/mydir", 0755)
|
||||
// result := mkdirOp() // Either[error, string]
|
||||
func Mkdir(path string, perm os.FileMode) IOEither[error, string] {
|
||||
return ioeither.TryCatchError(func() (string, error) {
|
||||
return path, os.Mkdir(path, perm)
|
||||
|
||||
153
v2/ioeither/file/dir_test.go
Normal file
153
v2/ioeither/file/dir_test.go
Normal file
@@ -0,0 +1,153 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMkdirAll(t *testing.T) {
|
||||
// Create a temporary directory for testing
|
||||
tempDir := t.TempDir()
|
||||
|
||||
t.Run("creates nested directories", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "nested", "dir", "structure")
|
||||
|
||||
result := MkdirAll(testPath, 0755)()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
path := E.GetOrElse(func(error) string { return "" })(result)
|
||||
assert.Equal(t, testPath, path)
|
||||
|
||||
// Verify directory was created
|
||||
info, err := os.Stat(testPath)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, info.IsDir())
|
||||
})
|
||||
|
||||
t.Run("succeeds when directory already exists", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "existing")
|
||||
|
||||
// Create directory first
|
||||
err := os.Mkdir(testPath, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try to create again
|
||||
result := MkdirAll(testPath, 0755)()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
path := E.GetOrElse(func(error) string { return "" })(result)
|
||||
assert.Equal(t, testPath, path)
|
||||
})
|
||||
|
||||
t.Run("fails with invalid path", func(t *testing.T) {
|
||||
// Use a path that contains a file as a parent
|
||||
filePath := filepath.Join(tempDir, "file.txt")
|
||||
err := os.WriteFile(filePath, []byte("test"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
invalidPath := filepath.Join(filePath, "subdir")
|
||||
result := MkdirAll(invalidPath, 0755)()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("creates directory with correct permissions", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "perms")
|
||||
|
||||
result := MkdirAll(testPath, 0700)()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify permissions (on Unix-like systems)
|
||||
info, err := os.Stat(testPath)
|
||||
require.NoError(t, err)
|
||||
// Note: actual permissions may differ due to umask
|
||||
assert.True(t, info.IsDir())
|
||||
})
|
||||
}
|
||||
|
||||
func TestMkdir(t *testing.T) {
|
||||
// Create a temporary directory for testing
|
||||
tempDir := t.TempDir()
|
||||
|
||||
t.Run("creates single directory", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "single")
|
||||
|
||||
result := Mkdir(testPath, 0755)()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
path := E.GetOrElse(func(error) string { return "" })(result)
|
||||
assert.Equal(t, testPath, path)
|
||||
|
||||
// Verify directory was created
|
||||
info, err := os.Stat(testPath)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, info.IsDir())
|
||||
})
|
||||
|
||||
t.Run("fails when parent does not exist", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "nonexistent", "child")
|
||||
|
||||
result := Mkdir(testPath, 0755)()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails when directory already exists", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "existing2")
|
||||
|
||||
// Create directory first
|
||||
err := os.Mkdir(testPath, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try to create again
|
||||
result := Mkdir(testPath, 0755)()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails with invalid path", func(t *testing.T) {
|
||||
// Use a path that contains a file as a parent
|
||||
filePath := filepath.Join(tempDir, "file2.txt")
|
||||
err := os.WriteFile(filePath, []byte("test"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
invalidPath := filepath.Join(filePath, "subdir")
|
||||
result := Mkdir(invalidPath, 0755)()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("creates directory with correct permissions", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "perms2")
|
||||
|
||||
result := Mkdir(testPath, 0700)()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify permissions (on Unix-like systems)
|
||||
info, err := os.Stat(testPath)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, info.IsDir())
|
||||
})
|
||||
}
|
||||
@@ -90,8 +90,8 @@ func Remove(name string) IOEither[error, string] {
|
||||
}
|
||||
|
||||
// Close closes an object
|
||||
func Close[C io.Closer](c C) IOEither[error, any] {
|
||||
return ioeither.TryCatchError(func() (any, error) {
|
||||
return c, c.Close()
|
||||
func Close[C io.Closer](c C) IOEither[error, struct{}] {
|
||||
return ioeither.TryCatchError(func() (struct{}, error) {
|
||||
return struct{}{}, c.Close()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -248,9 +248,8 @@ func TestReadComposition(t *testing.T) {
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var num int
|
||||
// Simple parsing
|
||||
num = int(data[0]-'0')*10 + int(data[1]-'0')
|
||||
num := int(data[0]-'0')*10 + int(data[1]-'0')
|
||||
return num, nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -28,7 +28,16 @@ var (
|
||||
readAll = ioeither.Eitherize1(io.ReadAll)
|
||||
)
|
||||
|
||||
// ReadAll uses a generator function to create a stream, reads it and closes it
|
||||
// ReadAll reads all data from a ReadCloser and ensures it is properly closed.
|
||||
// It takes an IOEither that acquires the ReadCloser, reads all its content until EOF,
|
||||
// and automatically closes the reader, even if an error occurs during reading.
|
||||
//
|
||||
// This is the recommended way to read entire files with proper resource management.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// readOp := ReadAll(Open("input.txt"))
|
||||
// result := readOp() // Either[error, []byte]
|
||||
func ReadAll[R io.ReadCloser](acquire IOEither[error, R]) IOEither[error, []byte] {
|
||||
return F.Pipe1(
|
||||
F.Flow2(
|
||||
|
||||
150
v2/ioeither/file/readall_test.go
Normal file
150
v2/ioeither/file/readall_test.go
Normal file
@@ -0,0 +1,150 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestReadAll(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
t.Run("reads entire file", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "readall.txt")
|
||||
testData := []byte("Hello, ReadAll!")
|
||||
|
||||
// Create test file
|
||||
err := os.WriteFile(testPath, testData, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Read file
|
||||
result := ReadAll(Open(testPath))()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
data := E.GetOrElse(func(error) []byte { return nil })(result)
|
||||
assert.Equal(t, testData, data)
|
||||
})
|
||||
|
||||
t.Run("reads empty file", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "empty.txt")
|
||||
|
||||
// Create empty file
|
||||
err := os.WriteFile(testPath, []byte{}, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Read file
|
||||
result := ReadAll(Open(testPath))()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
data := E.GetOrElse(func(error) []byte { return nil })(result)
|
||||
assert.Equal(t, 0, len(data))
|
||||
})
|
||||
|
||||
t.Run("reads large file", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "large.txt")
|
||||
|
||||
// Create large file (1MB)
|
||||
largeData := make([]byte, 1024*1024)
|
||||
for i := range largeData {
|
||||
largeData[i] = byte(i % 256)
|
||||
}
|
||||
err := os.WriteFile(testPath, largeData, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Read file
|
||||
result := ReadAll(Open(testPath))()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
data := E.GetOrElse(func(error) []byte { return nil })(result)
|
||||
assert.Equal(t, len(largeData), len(data))
|
||||
assert.Equal(t, largeData, data)
|
||||
})
|
||||
|
||||
t.Run("fails when file does not exist", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "nonexistent.txt")
|
||||
|
||||
// Try to read non-existent file
|
||||
result := ReadAll(Open(testPath))()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails when trying to read directory", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "dir")
|
||||
err := os.Mkdir(testPath, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try to read directory
|
||||
result := ReadAll(Open(testPath))()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("reads file with special characters", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "special.txt")
|
||||
testData := []byte("Hello\nWorld\t!\r\n")
|
||||
|
||||
// Create test file
|
||||
err := os.WriteFile(testPath, testData, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Read file
|
||||
result := ReadAll(Open(testPath))()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
data := E.GetOrElse(func(error) []byte { return nil })(result)
|
||||
assert.Equal(t, testData, data)
|
||||
})
|
||||
|
||||
t.Run("reads binary file", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "binary.bin")
|
||||
testData := []byte{0x00, 0x01, 0x02, 0xFF, 0xFE, 0xFD}
|
||||
|
||||
// Create binary file
|
||||
err := os.WriteFile(testPath, testData, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Read file
|
||||
result := ReadAll(Open(testPath))()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
data := E.GetOrElse(func(error) []byte { return nil })(result)
|
||||
assert.Equal(t, testData, data)
|
||||
})
|
||||
|
||||
t.Run("closes file after reading", func(t *testing.T) {
|
||||
testPath := filepath.Join(tempDir, "close_test.txt")
|
||||
testData := []byte("test")
|
||||
|
||||
// Create test file
|
||||
err := os.WriteFile(testPath, testData, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Read file
|
||||
result := ReadAll(Open(testPath))()
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify we can delete the file (it's closed)
|
||||
err = os.Remove(testPath)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user