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

Compare commits

...

3 Commits

Author SHA1 Message Date
Dr. Carsten Leue
ba10d8d314 doc: fix docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-09 13:00:03 +01:00
Dr. Carsten Leue
3d6c419185 fix: add better logging
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-09 12:49:44 +01:00
Dr. Carsten Leue
3f4b6292e4 fix: optimize Traverse
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-05 21:35:05 +01:00
48 changed files with 9233 additions and 69 deletions

View File

@@ -24,8 +24,8 @@ import (
// withContext wraps an existing IOEither and performs a context check for cancellation before delegating
func WithContext[A any](ctx context.Context, ma IOResult[A]) IOResult[A] {
return func() Result[A] {
if err := context.Cause(ctx); err != nil {
return result.Left[A](err)
if ctx.Err() != nil {
return result.Left[A](context.Cause(ctx))
}
return ma()
}

View File

@@ -0,0 +1,16 @@
package readerio
import (
RIO "github.com/IBM/fp-go/v2/readerio"
)
//go:inline
func Bracket[
A, B, ANY any](
acquire ReaderIO[A],
use Kleisli[A, B],
release func(A, B) ReaderIO[ANY],
) ReaderIO[B] {
return RIO.Bracket(acquire, use, release)
}

View File

@@ -16,5 +16,5 @@ func SequenceReader[R, A any](ma ReaderIO[Reader[R, A]]) Reader[R, ReaderIO[A]]
func TraverseReader[R, A, B any](
f reader.Kleisli[R, A, B],
) func(ReaderIO[A]) Kleisli[R, B] {
return RIO.TraverseReader[context.Context, R](f)
return RIO.TraverseReader[context.Context](f)
}

View File

@@ -17,6 +17,7 @@ package readerio
import (
"context"
"time"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/reader"
@@ -558,3 +559,197 @@ func TapReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, A
func Read[A any](r context.Context) func(ReaderIO[A]) IO[A] {
return RIO.Read[A](r)
}
// Local transforms the context.Context environment before passing it to a ReaderIO computation.
//
// This is the Reader's local operation, which allows you to modify the environment
// for a specific computation without affecting the outer context. The transformation
// function receives the current context and returns a new context along with a
// cancel function. The cancel function is automatically called when the computation
// completes (via defer), ensuring proper cleanup of resources.
//
// This is useful for:
// - Adding timeouts or deadlines to specific operations
// - Adding context values for nested computations
// - Creating isolated context scopes
// - Implementing context-based dependency injection
//
// Type Parameters:
// - A: The value type of the ReaderIO
//
// Parameters:
// - f: A function that transforms the context and returns a cancel function
//
// Returns:
// - An Operator that runs the computation with the transformed context
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// // Add a custom value to the context
// type key int
// const userKey key = 0
//
// addUser := readerio.Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
// newCtx := context.WithValue(ctx, userKey, "Alice")
// return newCtx, func() {} // No-op cancel
// })
//
// getUser := readerio.FromReader(func(ctx context.Context) string {
// if user := ctx.Value(userKey); user != nil {
// return user.(string)
// }
// return "unknown"
// })
//
// result := F.Pipe1(
// getUser,
// addUser,
// )
// user := result(context.Background())() // Returns "Alice"
//
// Timeout Example:
//
// // Add a 5-second timeout to a specific operation
// withTimeout := readerio.Local[Data](func(ctx context.Context) (context.Context, context.CancelFunc) {
// return context.WithTimeout(ctx, 5*time.Second)
// })
//
// result := F.Pipe1(
// fetchData,
// withTimeout,
// )
func Local[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
return func(rr ReaderIO[A]) ReaderIO[A] {
return func(ctx context.Context) IO[A] {
return func() A {
otherCtx, otherCancel := f(ctx)
defer otherCancel()
return rr(otherCtx)()
}
}
}
}
// WithTimeout adds a timeout to the context for a ReaderIO computation.
//
// This is a convenience wrapper around Local that uses context.WithTimeout.
// The computation must complete within the specified duration, or it will be
// cancelled. This is useful for ensuring operations don't run indefinitely
// and for implementing timeout-based error handling.
//
// The timeout is relative to when the ReaderIO is executed, not when
// WithTimeout is called. The cancel function is automatically called when
// the computation completes, ensuring proper cleanup.
//
// Type Parameters:
// - A: The value type of the ReaderIO
//
// Parameters:
// - timeout: The maximum duration for the computation
//
// Returns:
// - An Operator that runs the computation with a timeout
//
// Example:
//
// import (
// "time"
// F "github.com/IBM/fp-go/v2/function"
// )
//
// // Fetch data with a 5-second timeout
// fetchData := readerio.FromReader(func(ctx context.Context) Data {
// // Simulate slow operation
// select {
// case <-time.After(10 * time.Second):
// return Data{Value: "slow"}
// case <-ctx.Done():
// return Data{}
// }
// })
//
// result := F.Pipe1(
// fetchData,
// readerio.WithTimeout[Data](5*time.Second),
// )
// data := result(context.Background())() // Returns Data{} after 5s timeout
//
// Successful Example:
//
// quickFetch := readerio.Of(Data{Value: "quick"})
// result := F.Pipe1(
// quickFetch,
// readerio.WithTimeout[Data](5*time.Second),
// )
// data := result(context.Background())() // Returns Data{Value: "quick"}
func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, timeout)
})
}
// WithDeadline adds an absolute deadline to the context for a ReaderIO computation.
//
// This is a convenience wrapper around Local that uses context.WithDeadline.
// The computation must complete before the specified time, or it will be
// cancelled. This is useful for coordinating operations that must finish
// by a specific time, such as request deadlines or scheduled tasks.
//
// The deadline is an absolute time, unlike WithTimeout which uses a relative
// duration. The cancel function is automatically called when the computation
// completes, ensuring proper cleanup.
//
// Type Parameters:
// - A: The value type of the ReaderIO
//
// Parameters:
// - deadline: The absolute time by which the computation must complete
//
// Returns:
// - An Operator that runs the computation with a deadline
//
// Example:
//
// import (
// "time"
// F "github.com/IBM/fp-go/v2/function"
// )
//
// // Operation must complete by 3 PM
// deadline := time.Date(2024, 1, 1, 15, 0, 0, 0, time.UTC)
//
// fetchData := readerio.FromReader(func(ctx context.Context) Data {
// // Simulate operation
// select {
// case <-time.After(1 * time.Hour):
// return Data{Value: "done"}
// case <-ctx.Done():
// return Data{}
// }
// })
//
// result := F.Pipe1(
// fetchData,
// readerio.WithDeadline[Data](deadline),
// )
// data := result(context.Background())() // Returns Data{} if past deadline
//
// Combining with Parent Context:
//
// // If parent context already has a deadline, the earlier one takes precedence
// parentCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Hour))
// defer cancel()
//
// laterDeadline := time.Now().Add(2 * time.Hour)
// result := F.Pipe1(
// fetchData,
// readerio.WithDeadline[Data](laterDeadline),
// )
// data := result(parentCtx)() // Will use parent's 1-hour deadline
func WithDeadline[A any](deadline time.Time) Operator[A, A] {
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithDeadline(ctx, deadline)
})
}

View File

@@ -0,0 +1,379 @@
// Package readerioresult provides logging utilities for ReaderIOResult computations.
// It includes functions for entry/exit logging with timing, correlation IDs, and context management.
package readerioresult
import (
"context"
"log"
"sync/atomic"
"time"
"github.com/IBM/fp-go/v2/context/readerio"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/result"
)
type (
// loggingContextKeyType is the type used as a key for storing logging information in context.Context
loggingContextKeyType int
// LoggingID is a unique identifier assigned to each logged operation for correlation
LoggingID uint64
)
var (
// loggingContextKey is the singleton key used to store/retrieve logging data from context
loggingContextKey loggingContextKeyType
// loggingCounter is an atomic counter that generates unique LoggingIDs
loggingCounter atomic.Uint64
// getLoggingContext retrieves the logging information (start time and ID) from the context.
// It returns a Pair containing the start time and the logging ID.
// This function assumes the context contains logging information; it will panic if not present.
getLoggingContext = function.Flow3(
function.Bind2nd(context.Context.Value, any(loggingContextKey)),
option.ToType[pair.Pair[time.Time, LoggingID]],
option.GetOrElse(function.Zero[pair.Pair[time.Time, LoggingID]]),
)
// getLoggingID extracts just the LoggingID from the context, discarding the start time.
// This is a convenience function composed from getLoggingContext and pair.Tail.
getLoggingID = function.Flow2(
getLoggingContext,
pair.Tail,
)
)
// WithLoggingID wraps a value with its associated LoggingID from the current context.
//
// This function retrieves the LoggingID from the context and pairs it with the provided value,
// creating a ReaderIOResult that produces a Pair[LoggingID, A]. This is useful when you need
// to correlate a value with the logging ID of the operation that produced it.
//
// Type Parameters:
// - A: The type of the value to be paired with the logging ID
//
// Parameters:
// - src: The value to be paired with the logging ID
//
// Returns:
// - A ReaderIOResult that produces a Pair containing the LoggingID and the source value
//
// Example:
//
// fetchUser := func(id int) ReaderIOResult[User] {
// return Of(User{ID: id, Name: "Alice"})
// }
//
// // Wrap the result with its logging ID
// withID := F.Pipe2(
// fetchUser(123),
// LogEntryExit[User]("fetchUser"),
// Chain(WithLoggingID[User]),
// )
//
// result := withID(ctx)() // Returns Result[Pair[LoggingID, User]]
// // Can now correlate the user with the operation that fetched it
//
// Use Cases:
// - Correlating results with the operations that produced them
// - Tracking data lineage through complex pipelines
// - Debugging by associating values with their source operations
// - Audit logging with operation correlation
func WithLoggingID[A any](src A) ReaderIOResult[pair.Pair[LoggingID, A]] {
return function.Pipe1(
Ask(),
Map(function.Flow2(
getLoggingID,
pair.FromTail[LoggingID](src),
)),
)
}
// LogEntryExitF creates a customizable operator that wraps a ReaderIOResult computation with entry/exit callbacks.
//
// This is a more flexible version of LogEntryExit that allows you to provide custom callbacks for
// entry and exit events. The onEntry callback receives the current context and can return a modified
// context (e.g., with additional logging information). The onExit callback receives the computation
// result and can perform custom logging, metrics collection, or cleanup.
//
// The function uses the bracket pattern to ensure that:
// - The onEntry callback is executed before the computation starts
// - The computation runs with the context returned by onEntry
// - The onExit callback is executed after the computation completes (success or failure)
// - The original result is preserved and returned unchanged
// - Cleanup happens even if the computation fails
//
// Type Parameters:
// - A: The success type of the ReaderIOResult
// - ANY: The return type of the onExit callback (typically any)
//
// Parameters:
// - onEntry: A ReaderIO that receives the current context and returns a (possibly modified) context.
// This is executed before the computation starts. Use this for logging entry, adding context values,
// starting timers, or initialization logic.
// - onExit: A Kleisli function that receives the Result[A] and returns a ReaderIO[ANY].
// This is executed after the computation completes, regardless of success or failure.
// Use this for logging exit, recording metrics, cleanup, or finalization logic.
//
// Returns:
// - An Operator that wraps the ReaderIOResult computation with the custom entry/exit callbacks
//
// Example with custom context modification:
//
// type RequestID string
//
// logOp := LogEntryExitF[User, any](
// func(ctx context.Context) IO[context.Context] {
// return func() context.Context {
// reqID := RequestID(uuid.New().String())
// log.Printf("[%s] Starting operation", reqID)
// return context.WithValue(ctx, "requestID", reqID)
// }
// },
// func(res Result[User]) ReaderIO[any] {
// return func(ctx context.Context) IO[any] {
// return func() any {
// reqID := ctx.Value("requestID").(RequestID)
// return function.Pipe1(
// res,
// result.Fold(
// func(err error) any {
// log.Printf("[%s] Operation failed: %v", reqID, err)
// return nil
// },
// func(_ User) any {
// log.Printf("[%s] Operation succeeded", reqID)
// return nil
// },
// ),
// )
// }
// }
// },
// )
//
// wrapped := logOp(fetchUser(123))
//
// Example with metrics collection:
//
// import "github.com/prometheus/client_golang/prometheus"
//
// metricsOp := LogEntryExitF[Response, any](
// func(ctx context.Context) IO[context.Context] {
// return func() context.Context {
// requestCount.WithLabelValues("api_call", "started").Inc()
// return context.WithValue(ctx, "startTime", time.Now())
// }
// },
// func(res Result[Response]) ReaderIO[any] {
// return func(ctx context.Context) IO[any] {
// return func() any {
// startTime := ctx.Value("startTime").(time.Time)
// duration := time.Since(startTime).Seconds()
//
// return function.Pipe1(
// res,
// result.Fold(
// func(err error) any {
// requestCount.WithLabelValues("api_call", "error").Inc()
// requestDuration.WithLabelValues("api_call", "error").Observe(duration)
// return nil
// },
// func(_ Response) any {
// requestCount.WithLabelValues("api_call", "success").Inc()
// requestDuration.WithLabelValues("api_call", "success").Observe(duration)
// return nil
// },
// ),
// )
// }
// }
// },
// )
//
// Use Cases:
// - Custom context modification: Adding request IDs, trace IDs, or other context values
// - Structured logging: Integration with zap, logrus, or other structured loggers
// - Metrics collection: Recording operation durations, success/failure rates
// - Distributed tracing: OpenTelemetry, Jaeger integration
// - Custom monitoring: Application-specific monitoring and alerting
//
// Note: LogEntryExit is implemented using LogEntryExitF with standard logging and context management.
// Use LogEntryExitF when you need more control over the entry/exit behavior or context modification.
func LogEntryExitF[A, ANY any](
onEntry ReaderIO[context.Context],
onExit readerio.Kleisli[Result[A], ANY],
) Operator[A, A] {
bracket := function.Bind13of3(readerio.Bracket[context.Context, Result[A], ANY])(onEntry, func(newCtx context.Context, res Result[A]) ReaderIO[ANY] {
return readerio.FromIO(onExit(res)(newCtx)) // Get the exit callback for this result
})
return func(src ReaderIOResult[A]) ReaderIOResult[A] {
return bracket(function.Flow2(
src,
FromIOResult,
))
}
}
// LogEntryExit creates an operator that logs the entry and exit of a ReaderIOResult computation with timing and correlation IDs.
//
// This function wraps a ReaderIOResult computation with automatic logging that tracks:
// - Entry: Logs when the computation starts with "[entering <id>] <name>"
// - Exit: Logs when the computation completes successfully with "[exiting <id>] <name> [duration]"
// - Error: Logs when the computation fails with "[throwing <id>] <name> [duration]: <error>"
//
// Each logged operation is assigned a unique LoggingID (a monotonically increasing counter) that
// appears in all log messages for that operation. This ID enables correlation of entry and exit
// logs, even when multiple operations are running concurrently or are interleaved.
//
// The logging information (start time and ID) is stored in the context and can be retrieved using
// getLoggingContext or getLoggingID. This allows nested operations to access the parent operation's
// logging information.
//
// Type Parameters:
// - A: The success type of the ReaderIOResult
//
// Parameters:
// - name: A descriptive name for the computation, used in log messages to identify the operation
//
// Returns:
// - An Operator that wraps the ReaderIOResult computation with entry/exit logging
//
// The function uses the bracket pattern to ensure that:
// - Entry is logged before the computation starts
// - A unique LoggingID is assigned and stored in the context
// - Exit/error is logged after the computation completes, regardless of success or failure
// - Timing is accurate, measuring from entry to exit
// - The original result is preserved and returned unchanged
//
// Log Format:
// - Entry: "[entering <id>] <name>"
// - Success: "[exiting <id>] <name> [<duration>s]"
// - Error: "[throwing <id>] <name> [<duration>s]: <error>"
//
// Example with successful computation:
//
// fetchUser := func(id int) ReaderIOResult[User] {
// return Of(User{ID: id, Name: "Alice"})
// }
//
// // Wrap with logging
// loggedFetch := LogEntryExit[User]("fetchUser")(fetchUser(123))
//
// // Execute
// result := loggedFetch(context.Background())()
// // Logs:
// // [entering 1] fetchUser
// // [exiting 1] fetchUser [0.1s]
//
// Example with error:
//
// failingOp := func() ReaderIOResult[string] {
// return Left[string](errors.New("connection timeout"))
// }
//
// logged := LogEntryExit[string]("failingOp")(failingOp())
// result := logged(context.Background())()
// // Logs:
// // [entering 2] failingOp
// // [throwing 2] failingOp [0.0s]: connection timeout
//
// Example with nested operations:
//
// fetchOrders := func(userID int) ReaderIOResult[[]Order] {
// return Of([]Order{{ID: 1}})
// }
//
// pipeline := F.Pipe3(
// fetchUser(123),
// LogEntryExit[User]("fetchUser"),
// Chain(func(user User) ReaderIOResult[[]Order] {
// return fetchOrders(user.ID)
// }),
// LogEntryExit[[]Order]("fetchOrders"),
// )
//
// result := pipeline(context.Background())()
// // Logs:
// // [entering 3] fetchUser
// // [exiting 3] fetchUser [0.1s]
// // Fetching orders for user (parent operation: 3)
// // [entering 4] fetchOrders
// // [exiting 4] fetchOrders [0.2s]
//
// Example with concurrent operations:
//
// // Multiple operations can run concurrently, each with unique IDs
// op1 := LogEntryExit[Data]("operation1")(fetchData(1))
// op2 := LogEntryExit[Data]("operation2")(fetchData(2))
//
// go op1(context.Background())()
// go op2(context.Background())()
// // Logs (order may vary):
// // [entering 5] operation1
// // [entering 6] operation2
// // [exiting 5] operation1 [0.1s]
// // [exiting 6] operation2 [0.2s]
// // The IDs allow correlation even when logs are interleaved
//
// Use Cases:
// - Debugging: Track execution flow through complex ReaderIOResult chains with correlation IDs
// - Performance monitoring: Identify slow operations with timing information
// - Production logging: Monitor critical operations with unique identifiers
// - Concurrent operations: Correlate logs from multiple concurrent operations
// - Nested operations: Track parent-child relationships in operation hierarchies
// - Troubleshooting: Quickly identify where errors occur and correlate with entry logs
//
// Note: This function uses Go's standard log package and a global atomic counter for IDs.
// For production systems, consider using a structured logging library and adapting this
// pattern to support different log levels, structured fields, and distributed tracing.
func LogEntryExit[A any](name string) Operator[A, A] {
return LogEntryExitF(
func(ctx context.Context) IO[context.Context] {
return func() context.Context {
// Generate unique logging ID and capture start time
counter := LoggingID(loggingCounter.Add(1))
tStart := time.Now()
// Log entry with unique ID
log.Printf("[entering %d] %s", counter, name)
// Store logging information in context for later retrieval
return context.WithValue(ctx, loggingContextKey, pair.MakePair(tStart, counter))
}
},
func(res Result[A]) ReaderIO[any] {
return func(ctx context.Context) IO[any] {
value := getLoggingContext(ctx)
counter := pair.Tail(value)
return func() any {
// Retrieve logging information from context
duration := time.Since(pair.Head(value)).Seconds()
// Log error with ID and duration
onError := func(err error) any {
log.Printf("[throwing %d] %s [%.1fs]: %v", counter, name, duration, err)
return nil
}
// Log success with ID and duration
onSuccess := func(_ A) any {
log.Printf("[exiting %d] %s [%.1fs]", counter, name, duration)
return nil
}
return function.Pipe1(
res,
result.Fold(onError, onSuccess),
)
}
}
},
)
}

View File

@@ -0,0 +1,39 @@
package readerioresult
import (
"strings"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
func TestLoggingContext(t *testing.T) {
data := F.Pipe2(
Of("Sample"),
LogEntryExit[string]("TestLoggingContext1"),
LogEntryExit[string]("TestLoggingContext2"),
)
assert.Equal(t, result.Of("Sample"), data(t.Context())())
}
func TestLoggingContextWithLogger(t *testing.T) {
data := F.Pipe4(
Of("Sample"),
LogEntryExit[string]("TestLoggingContext1"),
Map(strings.ToUpper),
ChainFirst(F.Flow2(
WithLoggingID[string],
ChainIOK(io.Logf[pair.Pair[LoggingID, string]]("Prefix: %s")),
)),
LogEntryExit[string]("TestLoggingContext2"),
)
assert.Equal(t, result.Of("SAMPLE"), data(t.Context())())
}

View File

@@ -30,6 +30,7 @@ import (
"github.com/IBM/fp-go/v2/readerio"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/readeroption"
"github.com/IBM/fp-go/v2/result"
)
const (
@@ -243,14 +244,14 @@ func MonadApPar[B, A any](fab ReaderIOResult[func(A) B], fa ReaderIOResult[A]) R
return func(ctx context.Context) IOResult[B] {
// quick check for cancellation
if err := context.Cause(ctx); err != nil {
return ioeither.Left[B](err)
if ctx.Err() != nil {
return ioeither.Left[B](context.Cause(ctx))
}
return func() Result[B] {
// quick check for cancellation
if err := context.Cause(ctx); err != nil {
return either.Left[B](err)
if ctx.Err() != nil {
return either.Left[B](context.Cause(ctx))
}
// create sub-contexts for fa and fab, so they can cancel one other
@@ -958,3 +959,205 @@ func ChainFirstLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
func TapLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
return RIOR.TapLeft[A](f)
}
// Local transforms the context.Context environment before passing it to a ReaderIOResult computation.
//
// This is the Reader's local operation, which allows you to modify the environment
// for a specific computation without affecting the outer context. The transformation
// function receives the current context and returns a new context along with a
// cancel function. The cancel function is automatically called when the computation
// completes (via defer), ensuring proper cleanup of resources.
//
// The function checks for context cancellation before applying the transformation,
// returning an error immediately if the context is already cancelled.
//
// This is useful for:
// - Adding timeouts or deadlines to specific operations
// - Adding context values for nested computations
// - Creating isolated context scopes
// - Implementing context-based dependency injection
//
// Type Parameters:
// - A: The value type of the ReaderIOResult
//
// Parameters:
// - f: A function that transforms the context and returns a cancel function
//
// Returns:
// - An Operator that runs the computation with the transformed context
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// // Add a custom value to the context
// type key int
// const userKey key = 0
//
// addUser := readerioresult.Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
// newCtx := context.WithValue(ctx, userKey, "Alice")
// return newCtx, func() {} // No-op cancel
// })
//
// getUser := readerioresult.FromReader(func(ctx context.Context) string {
// if user := ctx.Value(userKey); user != nil {
// return user.(string)
// }
// return "unknown"
// })
//
// result := F.Pipe1(
// getUser,
// addUser,
// )
// value, err := result(context.Background())() // Returns ("Alice", nil)
//
// Timeout Example:
//
// // Add a 5-second timeout to a specific operation
// withTimeout := readerioresult.Local[Data](func(ctx context.Context) (context.Context, context.CancelFunc) {
// return context.WithTimeout(ctx, 5*time.Second)
// })
//
// result := F.Pipe1(
// fetchData,
// withTimeout,
// )
func Local[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
return func(rr ReaderIOResult[A]) ReaderIOResult[A] {
return func(ctx context.Context) IOResult[A] {
return func() Result[A] {
if ctx.Err() != nil {
return result.Left[A](context.Cause(ctx))
}
otherCtx, otherCancel := f(ctx)
defer otherCancel()
return rr(otherCtx)()
}
}
}
}
// WithTimeout adds a timeout to the context for a ReaderIOResult computation.
//
// This is a convenience wrapper around Local that uses context.WithTimeout.
// The computation must complete within the specified duration, or it will be
// cancelled. This is useful for ensuring operations don't run indefinitely
// and for implementing timeout-based error handling.
//
// The timeout is relative to when the ReaderIOResult is executed, not when
// WithTimeout is called. The cancel function is automatically called when
// the computation completes, ensuring proper cleanup. If the timeout expires,
// the computation will receive a context.DeadlineExceeded error.
//
// Type Parameters:
// - A: The value type of the ReaderIOResult
//
// Parameters:
// - timeout: The maximum duration for the computation
//
// Returns:
// - An Operator that runs the computation with a timeout
//
// Example:
//
// import (
// "time"
// F "github.com/IBM/fp-go/v2/function"
// )
//
// // Fetch data with a 5-second timeout
// fetchData := readerioresult.FromReader(func(ctx context.Context) Data {
// // Simulate slow operation
// select {
// case <-time.After(10 * time.Second):
// return Data{Value: "slow"}
// case <-ctx.Done():
// return Data{}
// }
// })
//
// result := F.Pipe1(
// fetchData,
// readerioresult.WithTimeout[Data](5*time.Second),
// )
// value, err := result(context.Background())() // Returns (Data{}, context.DeadlineExceeded) after 5s
//
// Successful Example:
//
// quickFetch := readerioresult.Right(Data{Value: "quick"})
// result := F.Pipe1(
// quickFetch,
// readerioresult.WithTimeout[Data](5*time.Second),
// )
// value, err := result(context.Background())() // Returns (Data{Value: "quick"}, nil)
func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, timeout)
})
}
// WithDeadline adds an absolute deadline to the context for a ReaderIOResult computation.
//
// This is a convenience wrapper around Local that uses context.WithDeadline.
// The computation must complete before the specified time, or it will be
// cancelled. This is useful for coordinating operations that must finish
// by a specific time, such as request deadlines or scheduled tasks.
//
// The deadline is an absolute time, unlike WithTimeout which uses a relative
// duration. The cancel function is automatically called when the computation
// completes, ensuring proper cleanup. If the deadline passes, the computation
// will receive a context.DeadlineExceeded error.
//
// Type Parameters:
// - A: The value type of the ReaderIOResult
//
// Parameters:
// - deadline: The absolute time by which the computation must complete
//
// Returns:
// - An Operator that runs the computation with a deadline
//
// Example:
//
// import (
// "time"
// F "github.com/IBM/fp-go/v2/function"
// )
//
// // Operation must complete by 3 PM
// deadline := time.Date(2024, 1, 1, 15, 0, 0, 0, time.UTC)
//
// fetchData := readerioresult.FromReader(func(ctx context.Context) Data {
// // Simulate operation
// select {
// case <-time.After(1 * time.Hour):
// return Data{Value: "done"}
// case <-ctx.Done():
// return Data{}
// }
// })
//
// result := F.Pipe1(
// fetchData,
// readerioresult.WithDeadline[Data](deadline),
// )
// value, err := result(context.Background())() // Returns (Data{}, context.DeadlineExceeded) if past deadline
//
// Combining with Parent Context:
//
// // If parent context already has a deadline, the earlier one takes precedence
// parentCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Hour))
// defer cancel()
//
// laterDeadline := time.Now().Add(2 * time.Hour)
// result := F.Pipe1(
// fetchData,
// readerioresult.WithDeadline[Data](laterDeadline),
// )
// value, err := result(parentCtx)() // Will use parent's 1-hour deadline
func WithDeadline[A any](deadline time.Time) Operator[A, A] {
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithDeadline(ctx, deadline)
})
}

View File

@@ -16,7 +16,11 @@
package readerioresult
import (
"context"
"io"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/result"
)
// WithResource constructs a function that creates a resource, then operates on it and then releases the resource.
@@ -55,3 +59,111 @@ import (
func WithResource[A, R, ANY any](onCreate ReaderIOResult[R], onRelease Kleisli[R, ANY]) Kleisli[Kleisli[R, A], A] {
return RIOR.WithResource[A](onCreate, onRelease)
}
// onClose is a helper function that creates a ReaderIOResult for closing an io.Closer resource.
// It safely calls the Close() method and handles any errors that may occur during closing.
//
// Type Parameters:
// - A: Must implement io.Closer interface
//
// Parameters:
// - a: The resource to close
//
// Returns:
// - ReaderIOResult[any]: A computation that closes the resource and returns nil on success
//
// The function ignores the context parameter since closing operations typically don't need context.
// Any error from Close() is captured and returned as a Result error.
func onClose[A io.Closer](a A) ReaderIOResult[any] {
return func(_ context.Context) IOResult[any] {
return func() Result[any] {
return result.TryCatchError[any](nil, a.Close())
}
}
}
// WithCloser creates a resource management function specifically for io.Closer resources.
// This is a specialized version of WithResource that automatically handles closing of resources
// that implement the io.Closer interface.
//
// The function ensures that:
// - The resource is created using the onCreate function
// - The resource is automatically closed when the operation completes (success or failure)
// - Any errors during closing are properly handled
// - The resource is closed even if the main operation fails or the context is canceled
//
// Type Parameters:
// - B: The type of value returned by the resource-using function
// - A: The type of resource that implements io.Closer
//
// Parameters:
// - onCreate: ReaderIOResult that creates the io.Closer resource
//
// Returns:
// - A function that takes a resource-using function and returns a ReaderIOResult[B]
//
// Example with file operations:
//
// openFile := func(filename string) ReaderIOResult[*os.File] {
// return TryCatch(func(ctx context.Context) func() (*os.File, error) {
// return func() (*os.File, error) {
// return os.Open(filename)
// }
// })
// }
//
// fileReader := WithCloser(openFile("data.txt"))
// result := fileReader(func(f *os.File) ReaderIOResult[string] {
// return TryCatch(func(ctx context.Context) func() (string, error) {
// return func() (string, error) {
// data, err := io.ReadAll(f)
// return string(data), err
// }
// })
// })
//
// Example with HTTP response:
//
// httpGet := func(url string) ReaderIOResult[*http.Response] {
// return TryCatch(func(ctx context.Context) func() (*http.Response, error) {
// return func() (*http.Response, error) {
// return http.Get(url)
// }
// })
// }
//
// responseReader := WithCloser(httpGet("https://api.example.com/data"))
// result := responseReader(func(resp *http.Response) ReaderIOResult[[]byte] {
// return TryCatch(func(ctx context.Context) func() ([]byte, error) {
// return func() ([]byte, error) {
// return io.ReadAll(resp.Body)
// }
// })
// })
//
// Example with database connection:
//
// openDB := func(dsn string) ReaderIOResult[*sql.DB] {
// return TryCatch(func(ctx context.Context) func() (*sql.DB, error) {
// return func() (*sql.DB, error) {
// return sql.Open("postgres", dsn)
// }
// })
// }
//
// dbQuery := WithCloser(openDB("postgres://..."))
// result := dbQuery(func(db *sql.DB) ReaderIOResult[[]User] {
// return TryCatch(func(ctx context.Context) func() ([]User, error) {
// return func() ([]User, error) {
// rows, err := db.QueryContext(ctx, "SELECT * FROM users")
// if err != nil {
// return nil, err
// }
// defer rows.Close()
// return scanUsers(rows)
// }
// })
// })
func WithCloser[B any, A io.Closer](onCreate ReaderIOResult[A]) Kleisli[Kleisli[A, B], B] {
return WithResource[B](onCreate, onClose[A])
}

View File

@@ -24,8 +24,8 @@ import (
// withContext wraps an existing ReaderResult and performs a context check for cancellation before deletating
func WithContext[A any](ma ReaderResult[A]) ReaderResult[A] {
return func(ctx context.Context) E.Either[error, A] {
if err := context.Cause(ctx); err != nil {
return E.Left[A](err)
if ctx.Err() != nil {
return E.Left[A](context.Cause(ctx))
}
return ma(ctx)
}

View File

@@ -53,7 +53,10 @@ func Identity[A any](a A) A {
//
// getMessage := Constant("Hello")
// msg := getMessage() // "Hello"
//
//go:inline
func Constant[A any](a A) func() A {
//go:inline
return func() A {
return a
}
@@ -81,7 +84,10 @@ func Constant[A any](a A) func() A {
//
// defaultName := Constant1[int, string]("Unknown")
// name := defaultName(42) // "Unknown"
//
//go:inline
func Constant1[B, A any](a A) func(B) A {
//go:inline
return func(_ B) A {
return a
}
@@ -107,7 +113,10 @@ func Constant1[B, A any](a A) func(B) A {
//
// alwaysTrue := Constant2[int, string, bool](true)
// result := alwaysTrue(42, "test") // true
//
//go:inline
func Constant2[B, C, A any](a A) func(B, C) A {
//go:inline
return func(_ B, _ C) A {
return a
}
@@ -128,6 +137,8 @@ func Constant2[B, C, A any](a A) func(B, C) A {
//
// value := 42
// IsNil(&value) // false
//
//go:inline
func IsNil[A any](a *A) bool {
return a == nil
}
@@ -149,6 +160,8 @@ func IsNil[A any](a *A) bool {
//
// value := 42
// IsNonNil(&value) // true
//
//go:inline
func IsNonNil[A any](a *A) bool {
return a != nil
}
@@ -207,6 +220,8 @@ func Swap[T1, T2, R any](f func(T1, T2) R) func(T2, T1) R {
//
// result := First(42, "hello") // 42
// result := First(true, 100) // true
//
//go:inline
func First[T1, T2 any](t1 T1, _ T2) T1 {
return t1
}
@@ -231,6 +246,14 @@ func First[T1, T2 any](t1 T1, _ T2) T1 {
//
// result := Second(42, "hello") // "hello"
// result := Second(true, 100) // 100
//
//go:inline
func Second[T1, T2 any](_ T1, t2 T2) T2 {
return t2
}
// Zero returns the zero value of the given type.
func Zero[A comparable]() A {
var zero A
return zero
}

View File

@@ -0,0 +1,75 @@
// 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 (
RR "github.com/IBM/fp-go/v2/idiomatic/readerresult"
)
// TraverseArray applies a ReaderResult-returning function to each element of an array,
// collecting the results. If any element fails, the entire operation fails with the first error.
//
// Example:
//
// parseUser := func(id int) readerresult.ReaderResult[DB, User] { ... }
// ids := []int{1, 2, 3}
// result := readerresult.TraverseArray[DB](parseUser)(ids)
// // result(db) returns ([]User, nil) with all users or (nil, error) on first error
//
//go:inline
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
return RR.TraverseArray(f)
}
//go:inline
func MonadTraverseArray[A, B any](as []A, f Kleisli[A, B]) ReaderResult[[]B] {
return RR.MonadTraverseArray(as, f)
}
// TraverseArrayWithIndex is like TraverseArray but the function also receives the element's index.
// This is useful when the transformation depends on the position in the array.
//
// Example:
//
// processItem := func(idx int, item string) readerresult.ReaderResult[Config, int] {
// return readerresult.Of[Config](idx + len(item))
// }
// items := []string{"a", "bb", "ccc"}
// result := readerresult.TraverseArrayWithIndex[Config](processItem)(items)
//
//go:inline
func TraverseArrayWithIndex[A, B any](f func(int, A) ReaderResult[B]) Kleisli[[]A, []B] {
return RR.TraverseArrayWithIndex(f)
}
// SequenceArray converts an array of ReaderResult values into a single ReaderResult of an array.
// If any element fails, the entire operation fails with the first error encountered.
// All computations share the same environment.
//
// Example:
//
// readers := []readerresult.ReaderResult[Config, int]{
// readerresult.Of[Config](1),
// readerresult.Of[Config](2),
// readerresult.Of[Config](3),
// }
// result := readerresult.SequenceArray(readers)
// // result(cfg) returns ([]int{1, 2, 3}, nil)
//
//go:inline
func SequenceArray[A any](ma []ReaderResult[A]) ReaderResult[[]A] {
return RR.SequenceArray(ma)
}

View File

@@ -0,0 +1,337 @@
// 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/idiomatic/readerresult"
"github.com/IBM/fp-go/v2/idiomatic/result"
C "github.com/IBM/fp-go/v2/internal/chain"
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/reader"
RES "github.com/IBM/fp-go/v2/result"
)
// Do initializes a do-notation context with an empty state.
//
// This is the starting point for do-notation style composition, which allows
// imperative-style sequencing of ReaderResult computations while maintaining
// functional purity.
//
// Type Parameters:
// - S: The state type
//
// Parameters:
// - empty: The initial empty state
//
// Returns:
// - A ReaderResult[S] containing the initial state
//
// Example:
//
// type State struct {
// User User
// Posts []Post
// }
//
// result := F.Pipe2(
// readerresult.Do(State{}),
// readerresult.Bind(
// func(u User) func(State) State {
// return func(s State) State { s.User = u; return s }
// },
// func(s State) readerresult.ReaderResult[User] {
// return getUser(42)
// },
// ),
// readerresult.Bind(
// func(posts []Post) func(State) State {
// return func(s State) State { s.Posts = posts; return s }
// },
// func(s State) readerresult.ReaderResult[[]Post] {
// return getPosts(s.User.ID)
// },
// ),
// )
//
//go:inline
func Do[S any](
empty S,
) ReaderResult[S] {
return RR.Do[context.Context](empty)
}
// Bind sequences a ReaderResult computation and updates the state with its result.
//
// This is the core operation for do-notation, allowing you to chain computations
// where each step can depend on the accumulated state and update it with new values.
//
// Type Parameters:
// - S1: The input state type
// - S2: The output state type
// - T: The type of value produced by the computation
//
// Parameters:
// - setter: A function that takes the computation result and returns a state updater
// - f: A Kleisli arrow that produces the next computation based on current state
//
// Returns:
// - An Operator that transforms ReaderResult[S1] to ReaderResult[S2]
//
// Example:
//
// readerresult.Bind(
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// func(s State) readerresult.ReaderResult[User] {
// return getUser(s.UserID)
// },
// )
//
//go:inline
func Bind[S1, S2, T any](
setter func(T) func(S1) S2,
f Kleisli[S1, T],
) Operator[S1, S2] {
return C.Bind(
Chain[S1, S2],
Map[T, S2],
setter,
f,
)
}
// Let attaches the result of a pure computation to a state.
//
// Unlike Bind, Let works with pure functions (not ReaderResult computations).
// This is useful for deriving values from the current state without performing
// any effects.
//
// Type Parameters:
// - S1: The input state type
// - S2: The output state type
// - T: The type of value computed
//
// Parameters:
// - setter: A function that takes the computed value and returns a state updater
// - f: A pure function that computes a value from the current state
//
// Returns:
// - An Operator that transforms ReaderResult[S1] to ReaderResult[S2]
//
// Example:
//
// readerresult.Let(
// func(fullName string) func(State) State {
// return func(s State) State { s.FullName = fullName; return s }
// },
// func(s State) string {
// return s.FirstName + " " + s.LastName
// },
// )
//
//go:inline
func Let[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) Operator[S1, S2] {
return RR.Let[context.Context](setter, f)
}
// LetTo attaches a constant value to a state.
//
// This is a simplified version of Let for when you want to add a constant
// value to the state without computing it.
//
// Type Parameters:
// - S1: The input state type
// - S2: The output state type
// - T: The type of the constant value
//
// Parameters:
// - setter: A function that takes the constant and returns a state updater
// - b: The constant value to attach
//
// Returns:
// - An Operator that transforms ReaderResult[S1] to ReaderResult[S2]
//
// Example:
//
// readerresult.LetTo(
// func(status string) func(State) State {
// return func(s State) State { s.Status = status; return s }
// },
// "active",
// )
//
//go:inline
func LetTo[S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) Operator[S1, S2] {
return RR.LetTo[context.Context](setter, b)
}
// BindTo initializes do-notation by binding a value to a state.
//
// This is typically used as the first operation after a computation to
// start building up a state structure.
//
// Type Parameters:
// - S1: The state type to create
// - T: The type of the initial value
//
// Parameters:
// - setter: A function that creates the initial state from a value
//
// Returns:
// - An Operator that transforms ReaderResult[T] to ReaderResult[S1]
//
// Example:
//
// type State struct {
// User User
// }
//
// result := F.Pipe1(
// getUser(42),
// readerresult.BindTo(func(u User) State {
// return State{User: u}
// }),
// )
//
//go:inline
func BindTo[S1, T any](
setter func(T) S1,
) Operator[T, S1] {
return RR.BindTo[context.Context](setter)
}
//go:inline
func ApS[S1, S2, T any](
setter func(T) func(S1) S2,
fa ReaderResult[T],
) Operator[S1, S2] {
return RR.ApS[context.Context](setter, fa)
}
//go:inline
func ApSL[S, T any](
lens L.Lens[S, T],
fa ReaderResult[T],
) Operator[S, S] {
return ApSL(lens, fa)
}
//go:inline
func BindL[S, T any](
lens L.Lens[S, T],
f Kleisli[T, T],
) Operator[S, S] {
return RR.BindL(lens, f)
}
//go:inline
func LetL[S, T any](
lens L.Lens[S, T],
f Endomorphism[T],
) Operator[S, S] {
return RR.LetL[context.Context](lens, f)
}
//go:inline
func LetToL[S, T any](
lens L.Lens[S, T],
b T,
) Operator[S, S] {
return RR.LetToL[context.Context](lens, b)
}
//go:inline
func BindReaderK[S1, S2, T any](
setter func(T) func(S1) S2,
f reader.Kleisli[context.Context, S1, T],
) Operator[S1, S2] {
return RR.BindReaderK(setter, f)
}
//go:inline
func BindEitherK[S1, S2, T any](
setter func(T) func(S1) S2,
f RES.Kleisli[S1, T],
) Operator[S1, S2] {
return RR.BindEitherK[context.Context](setter, f)
}
//go:inline
func BindResultK[S1, S2, T any](
setter func(T) func(S1) S2,
f result.Kleisli[S1, T],
) Operator[S1, S2] {
return RR.BindResultK[context.Context](setter, f)
}
//go:inline
func BindToReader[
S1, T any](
setter func(T) S1,
) func(Reader[context.Context, T]) ReaderResult[S1] {
return RR.BindToReader[context.Context](setter)
}
//go:inline
func BindToEither[
S1, T any](
setter func(T) S1,
) func(Result[T]) ReaderResult[S1] {
return RR.BindToEither[context.Context](setter)
}
//go:inline
func BindToResult[
S1, T any](
setter func(T) S1,
) func(T, error) ReaderResult[S1] {
return RR.BindToResult[context.Context](setter)
}
//go:inline
func ApReaderS[
S1, S2, T any](
setter func(T) func(S1) S2,
fa Reader[context.Context, T],
) Operator[S1, S2] {
return RR.ApReaderS(setter, fa)
}
//go:inline
func ApResultS[
S1, S2, T any](
setter func(T) func(S1) S2,
) func(T, error) Operator[S1, S2] {
return RR.ApResultS[context.Context](setter)
}
//go:inline
func ApEitherS[
S1, S2, T any](
setter func(T) func(S1) S2,
fa Result[T],
) Operator[S1, S2] {
return RR.ApEitherS[context.Context](setter, fa)
}

View File

@@ -0,0 +1,403 @@
// 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"
"io"
RR "github.com/IBM/fp-go/v2/idiomatic/readerresult"
)
// Bracket ensures safe resource management with guaranteed cleanup in the ReaderResult monad.
//
// This function implements the bracket pattern (also known as try-with-resources or RAII)
// for ReaderResult computations. It guarantees that the release action is called regardless
// of whether the use action succeeds or fails, making it ideal for managing resources like
// file handles, database connections, network sockets, or locks.
//
// The execution flow is:
// 1. Acquire the resource (lazily evaluated)
// 2. Use the resource with the provided function
// 3. Release the resource with access to: the resource, the result (if successful), and any error
//
// The release function is always called, even if:
// - The acquire action fails (release is not called in this case)
// - The use action fails (release receives the error)
// - The use action succeeds (release receives nil error)
//
// Type Parameters:
// - A: The type of the acquired resource
// - B: The type of the result produced by using the resource
// - ANY: The type returned by the release action (typically ignored)
//
// Parameters:
// - acquire: Lazy computation that acquires the resource
// - use: Function that uses the resource to produce a result
// - release: Function that releases the resource, receiving the resource, result, and any error
//
// Returns:
// - A ReaderResult[B] that safely manages the resource lifecycle
//
// Example - File handling:
//
// import (
// "context"
// "os"
// )
//
// readFile := readerresult.Bracket(
// // Acquire: Open file
// func() readerresult.ReaderResult[*os.File] {
// return func(ctx context.Context) (*os.File, error) {
// return os.Open("data.txt")
// }
// },
// // Use: Read file contents
// func(file *os.File) readerresult.ReaderResult[string] {
// return func(ctx context.Context) (string, error) {
// data, err := io.ReadAll(file)
// return string(data), err
// }
// },
// // Release: Close file (always called)
// func(file *os.File, content string, err error) readerresult.ReaderResult[any] {
// return func(ctx context.Context) (any, error) {
// return nil, file.Close()
// }
// },
// )
//
// content, err := readFile(context.Background())
//
// Example - Database connection:
//
// queryDB := readerresult.Bracket(
// // Acquire: Open connection
// func() readerresult.ReaderResult[*sql.DB] {
// return func(ctx context.Context) (*sql.DB, error) {
// return sql.Open("postgres", connString)
// }
// },
// // Use: Execute query
// func(db *sql.DB) readerresult.ReaderResult[[]User] {
// return func(ctx context.Context) ([]User, error) {
// return queryUsers(ctx, db)
// }
// },
// // Release: Close connection (always called)
// func(db *sql.DB, users []User, err error) readerresult.ReaderResult[any] {
// return func(ctx context.Context) (any, error) {
// return nil, db.Close()
// }
// },
// )
//
// Example - Lock management:
//
// withLock := readerresult.Bracket(
// // Acquire: Lock mutex
// func() readerresult.ReaderResult[*sync.Mutex] {
// return func(ctx context.Context) (*sync.Mutex, error) {
// mu.Lock()
// return mu, nil
// }
// },
// // Use: Perform critical section work
// func(mu *sync.Mutex) readerresult.ReaderResult[int] {
// return func(ctx context.Context) (int, error) {
// return performCriticalWork(ctx)
// }
// },
// // Release: Unlock mutex (always called)
// func(mu *sync.Mutex, result int, err error) readerresult.ReaderResult[any] {
// return func(ctx context.Context) (any, error) {
// mu.Unlock()
// return nil, nil
// }
// },
// )
func Bracket[
A, B, ANY any](
acquire Lazy[ReaderResult[A]],
use Kleisli[A, B],
release func(A, B, error) ReaderResult[ANY],
) ReaderResult[B] {
return RR.Bracket(acquire, use, release)
}
// WithResource creates a higher-order function for resource management with automatic cleanup.
//
// This function provides a more composable alternative to Bracket by creating a function
// that takes a resource-using function and automatically handles resource acquisition and
// release. This is particularly useful when you want to reuse the same resource management
// pattern with different operations.
//
// The pattern is:
// 1. Create a resource manager with onCreate and onRelease
// 2. Apply it to different use functions as needed
// 3. Each application ensures proper resource cleanup
//
// This is useful for:
// - Creating reusable resource management patterns
// - Building resource pools or factories
// - Composing resource-dependent operations
// - Abstracting resource lifecycle management
//
// Type Parameters:
// - B: The type of the result produced by using the resource
// - A: The type of the acquired resource
// - ANY: The type returned by the release action (typically ignored)
//
// Parameters:
// - onCreate: Lazy computation that creates/acquires the resource
// - onRelease: Function that releases the resource (receives the resource)
//
// Returns:
// - A Kleisli arrow that takes a resource-using function and returns a ReaderResult[B]
// with automatic resource management
//
// Example - Reusable database connection manager:
//
// import (
// "context"
// "database/sql"
// )
//
// // Create a reusable DB connection manager
// withDB := readerresult.WithResource(
// // onCreate: Acquire connection
// func() readerresult.ReaderResult[*sql.DB] {
// return func(ctx context.Context) (*sql.DB, error) {
// return sql.Open("postgres", connString)
// }
// },
// // onRelease: Close connection
// func(db *sql.DB) readerresult.ReaderResult[any] {
// return func(ctx context.Context) (any, error) {
// return nil, db.Close()
// }
// },
// )
//
// // Use the manager with different operations
// getUsers := withDB(func(db *sql.DB) readerresult.ReaderResult[[]User] {
// return func(ctx context.Context) ([]User, error) {
// return queryUsers(ctx, db)
// }
// })
//
// getOrders := withDB(func(db *sql.DB) readerresult.ReaderResult[[]Order] {
// return func(ctx context.Context) ([]Order, error) {
// return queryOrders(ctx, db)
// }
// })
//
// // Both operations automatically manage the connection
// users, err := getUsers(context.Background())
// orders, err := getOrders(context.Background())
//
// Example - File operations manager:
//
// withFile := readerresult.WithResource(
// func() readerresult.ReaderResult[*os.File] {
// return func(ctx context.Context) (*os.File, error) {
// return os.Open("config.json")
// }
// },
// func(file *os.File) readerresult.ReaderResult[any] {
// return func(ctx context.Context) (any, error) {
// return nil, file.Close()
// }
// },
// )
//
// // Different operations on the same file
// readConfig := withFile(func(file *os.File) readerresult.ReaderResult[Config] {
// return func(ctx context.Context) (Config, error) {
// return parseConfig(file)
// }
// })
//
// validateConfig := withFile(func(file *os.File) readerresult.ReaderResult[bool] {
// return func(ctx context.Context) (bool, error) {
// return validateConfigFile(file)
// }
// })
//
// Example - Composing with other operations:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// // Create a pipeline with automatic resource management
// processData := F.Pipe2(
// loadData,
// withDB(func(db *sql.DB) readerresult.ReaderResult[Result] {
// return saveToDatabase(db)
// }),
// readerresult.Map(formatResult),
// )
func WithResource[B, A, ANY any](
onCreate Lazy[ReaderResult[A]],
onRelease Kleisli[A, ANY],
) Kleisli[Kleisli[A, B], B] {
return RR.WithResource[B](onCreate, onRelease)
}
// onClose is a helper function that creates a ReaderResult that closes an io.Closer.
// This is used internally by WithCloser to provide automatic cleanup for resources
// that implement the io.Closer interface.
func onClose[A io.Closer](a A) ReaderResult[any] {
return func(_ context.Context) (any, error) {
return nil, a.Close()
}
}
// WithCloser creates a higher-order function for managing resources that implement io.Closer.
//
// This is a specialized version of WithResource that automatically handles cleanup for any
// resource implementing the io.Closer interface (such as files, network connections, HTTP
// response bodies, etc.). It eliminates the need to manually specify the release function,
// making it more convenient for common Go resources.
//
// The function automatically calls Close() on the resource when the operation completes,
// regardless of success or failure. This ensures proper resource cleanup following Go's
// standard io.Closer pattern.
//
// Type Parameters:
// - B: The type of the result produced by using the resource
// - A: The type of the resource, which must implement io.Closer
//
// Parameters:
// - onCreate: Lazy computation that creates/acquires the io.Closer resource
//
// Returns:
// - A Kleisli arrow that takes a resource-using function and returns a ReaderResult[B]
// with automatic Close() cleanup
//
// Example - File operations:
//
// import (
// "context"
// "os"
// "io"
// )
//
// // Create a reusable file manager
// withFile := readerresult.WithCloser(
// func() readerresult.ReaderResult[*os.File] {
// return func(ctx context.Context) (*os.File, error) {
// return os.Open("data.txt")
// }
// },
// )
//
// // Use with different operations - Close() is automatic
// readContent := withFile(func(file *os.File) readerresult.ReaderResult[string] {
// return func(ctx context.Context) (string, error) {
// data, err := io.ReadAll(file)
// return string(data), err
// }
// })
//
// getSize := withFile(func(file *os.File) readerresult.ReaderResult[int64] {
// return func(ctx context.Context) (int64, error) {
// info, err := file.Stat()
// if err != nil {
// return 0, err
// }
// return info.Size(), nil
// }
// })
//
// content, err := readContent(context.Background())
// size, err := getSize(context.Background())
//
// Example - HTTP response body:
//
// import "net/http"
//
// withResponse := readerresult.WithCloser(
// func() readerresult.ReaderResult[*http.Response] {
// return func(ctx context.Context) (*http.Response, error) {
// return http.Get("https://api.example.com/data")
// }
// },
// )
//
// // Body is automatically closed after use
// parseJSON := withResponse(func(resp *http.Response) readerresult.ReaderResult[Data] {
// return func(ctx context.Context) (Data, error) {
// var data Data
// err := json.NewDecoder(resp.Body).Decode(&data)
// return data, err
// }
// })
//
// Example - Multiple file operations:
//
// // Read from one file, write to another
// copyFile := func(src, dst string) readerresult.ReaderResult[int64] {
// withSrc := readerresult.WithCloser(
// func() readerresult.ReaderResult[*os.File] {
// return func(ctx context.Context) (*os.File, error) {
// return os.Open(src)
// }
// },
// )
//
// withDst := readerresult.WithCloser(
// func() readerresult.ReaderResult[*os.File] {
// return func(ctx context.Context) (*os.File, error) {
// return os.Create(dst)
// }
// },
// )
//
// return withSrc(func(srcFile *os.File) readerresult.ReaderResult[int64] {
// return withDst(func(dstFile *os.File) readerresult.ReaderResult[int64] {
// return func(ctx context.Context) (int64, error) {
// return io.Copy(dstFile, srcFile)
// }
// })
// })
// }
//
// Example - Network connection:
//
// import "net"
//
// withConn := readerresult.WithCloser(
// func() readerresult.ReaderResult[net.Conn] {
// return func(ctx context.Context) (net.Conn, error) {
// return net.Dial("tcp", "localhost:8080")
// }
// },
// )
//
// sendData := withConn(func(conn net.Conn) readerresult.ReaderResult[int] {
// return func(ctx context.Context) (int, error) {
// return conn.Write([]byte("Hello, World!"))
// }
// })
//
// Note: WithCloser is a convenience wrapper around WithResource that automatically
// provides the Close() cleanup function. For resources that don't implement io.Closer
// or require custom cleanup logic, use WithResource or Bracket instead.
func WithCloser[B any, A io.Closer](onCreate Lazy[ReaderResult[A]]) Kleisli[Kleisli[A, B], B] {
return WithResource[B](onCreate, onClose[A])
}

View File

@@ -0,0 +1,210 @@
// 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/idiomatic/readerresult"
)
// Curry0 converts a function that takes context.Context and returns (A, error) into a ReaderResult[A].
//
// This is useful for lifting existing functions that follow Go's context-first convention
// into the ReaderResult monad.
//
// Type Parameters:
// - A: The return value type
//
// Parameters:
// - f: A function that takes context.Context and returns (A, error)
//
// Returns:
// - A ReaderResult[A] that wraps the function
//
// Example:
//
// func getConfig(ctx context.Context) (Config, error) {
// // ... implementation
// return config, nil
// }
// rr := readerresult.Curry0(getConfig)
// config, err := rr(ctx)
//
//go:inline
func Curry0[A any](f func(context.Context) (A, error)) ReaderResult[A] {
return RR.Curry0(f)
}
// Curry1 converts a function with one parameter into a curried ReaderResult-returning function.
//
// The context.Context parameter is handled by the ReaderResult, allowing you to partially
// apply the business parameter before providing the context.
//
// Type Parameters:
// - T1: The first parameter type
// - A: The return value type
//
// Parameters:
// - f: A function that takes (context.Context, T1) and returns (A, error)
//
// Returns:
// - A curried function that takes T1 and returns ReaderResult[A]
//
// Example:
//
// func getUser(ctx context.Context, id int) (User, error) {
// // ... implementation
// return user, nil
// }
// getUserRR := readerresult.Curry1(getUser)
// rr := getUserRR(42) // Partially applied
// user, err := rr(ctx) // Execute with context
//
//go:inline
func Curry1[T1, A any](f func(context.Context, T1) (A, error)) func(T1) ReaderResult[A] {
return RR.Curry1(f)
}
// Curry2 converts a function with two parameters into a curried ReaderResult-returning function.
//
// The context.Context parameter is handled by the ReaderResult, allowing you to partially
// apply the business parameters before providing the context.
//
// Type Parameters:
// - T1: The first parameter type
// - T2: The second parameter type
// - A: The return value type
//
// Parameters:
// - f: A function that takes (context.Context, T1, T2) and returns (A, error)
//
// Returns:
// - A curried function that takes T1, then T2, and returns ReaderResult[A]
//
// Example:
//
// func updateUser(ctx context.Context, id int, name string) (User, error) {
// // ... implementation
// return user, nil
// }
// updateUserRR := readerresult.Curry2(updateUser)
// rr := updateUserRR(42)("Alice") // Partially applied
// user, err := rr(ctx) // Execute with context
//
//go:inline
func Curry2[T1, T2, A any](f func(context.Context, T1, T2) (A, error)) func(T1) func(T2) ReaderResult[A] {
return RR.Curry2(f)
}
// Curry3 converts a function with three parameters into a curried ReaderResult-returning function.
//
// The context.Context parameter is handled by the ReaderResult, allowing you to partially
// apply the business parameters before providing the context.
//
// Type Parameters:
// - T1: The first parameter type
// - T2: The second parameter type
// - T3: The third parameter type
// - A: The return value type
//
// Parameters:
// - f: A function that takes (context.Context, T1, T2, T3) and returns (A, error)
//
// Returns:
// - A curried function that takes T1, then T2, then T3, and returns ReaderResult[A]
//
// Example:
//
// func createPost(ctx context.Context, userID int, title string, body string) (Post, error) {
// // ... implementation
// return post, nil
// }
// createPostRR := readerresult.Curry3(createPost)
// rr := createPostRR(42)("Title")("Body") // Partially applied
// post, err := rr(ctx) // Execute with context
//
//go:inline
func Curry3[T1, T2, T3, A any](f func(context.Context, T1, T2, T3) (A, error)) func(T1) func(T2) func(T3) ReaderResult[A] {
return RR.Curry3(f)
}
// Uncurry1 converts a curried ReaderResult function back to a standard Go function.
//
// This is the inverse of Curry1, useful when you need to call curried functions
// in a traditional Go style.
//
// Type Parameters:
// - T1: The parameter type
// - A: The return value type
//
// Parameters:
// - f: A curried function that takes T1 and returns ReaderResult[A]
//
// Returns:
// - A function that takes (context.Context, T1) and returns (A, error)
//
// Example:
//
// curriedFn := func(id int) readerresult.ReaderResult[User] { ... }
// normalFn := readerresult.Uncurry1(curriedFn)
// user, err := normalFn(ctx, 42)
//
//go:inline
func Uncurry1[T1, A any](f func(T1) ReaderResult[A]) func(context.Context, T1) (A, error) {
return RR.Uncurry1(f)
}
// Uncurry2 converts a curried ReaderResult function with two parameters back to a standard Go function.
//
// This is the inverse of Curry2.
//
// Type Parameters:
// - T1: The first parameter type
// - T2: The second parameter type
// - A: The return value type
//
// Parameters:
// - f: A curried function that takes T1, then T2, and returns ReaderResult[A]
//
// Returns:
// - A function that takes (context.Context, T1, T2) and returns (A, error)
//
//go:inline
func Uncurry2[T1, T2, A any](f func(T1) func(T2) ReaderResult[A]) func(context.Context, T1, T2) (A, error) {
return RR.Uncurry2(f)
}
// Uncurry3 converts a curried ReaderResult function with three parameters back to a standard Go function.
//
// This is the inverse of Curry3.
//
// Type Parameters:
// - T1: The first parameter type
// - T2: The second parameter type
// - T3: The third parameter type
// - A: The return value type
//
// Parameters:
// - f: A curried function that takes T1, then T2, then T3, and returns ReaderResult[A]
//
// Returns:
// - A function that takes (context.Context, T1, T2, T3) and returns (A, error)
//
//go:inline
func Uncurry3[T1, T2, T3, A any](f func(T1) func(T2) func(T3) ReaderResult[A]) func(context.Context, T1, T2, T3) (A, error) {
return RR.Uncurry3(f)
}

View File

@@ -0,0 +1,178 @@
// 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 provides a ReaderResult monad that combines the Reader and Result monads.
//
// A ReaderResult[R, A] represents a computation that:
// - Depends on an environment of type R (Reader aspect)
// - May fail with an error (Result aspect, which is Either[error, A])
//
// This is equivalent to Reader[R, Result[A]] or Reader[R, Either[error, A]].
//
// # Use Cases
//
// ReaderResult is particularly useful for:
//
// 1. Dependency injection with error handling - pass configuration/services through
// computations that may fail
// 2. Functional error handling - compose operations that depend on context and may error
// 3. Testing - easily mock dependencies by changing the environment value
//
// # Basic Example
//
// type Config struct {
// DatabaseURL string
// }
//
// // Function that needs config and may fail
// func getUser(id int) readerresult.ReaderResult[Config, User] {
// return readerresult.Asks(func(cfg Config) result.Result[User] {
// // Use cfg.DatabaseURL to fetch user
// return result.Of(user)
// })
// }
//
// // Execute by providing the config
// cfg := Config{DatabaseURL: "postgres://..."}
// user, err := getUser(42)(cfg) // Returns (User, error)
//
// # Composition
//
// ReaderResult provides several ways to compose computations:
//
// 1. Map - transform successful values
// 2. Chain (FlatMap) - sequence dependent operations
// 3. Ap - combine independent computations
// 4. Do-notation - imperative-style composition with Bind
//
// # Do-Notation Example
//
// type State struct {
// User User
// Posts []Post
// }
//
// result := F.Pipe2(
// readerresult.Do[Config](State{}),
// readerresult.Bind(
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// func(s State) readerresult.ReaderResult[Config, User] {
// return getUser(42)
// },
// ),
// readerresult.Bind(
// func(posts []Post) func(State) State {
// return func(s State) State { s.Posts = posts; return s }
// },
// func(s State) readerresult.ReaderResult[Config, []Post] {
// return getPosts(s.User.ID)
// },
// ),
// )
//
// # Object-Oriented Patterns with Curry Functions
//
// The Curry functions enable an interesting pattern where you can treat the Reader context (R)
// as an object instance, effectively creating method-like functions that compose functionally.
//
// When you curry a function like func(R, T1, T2) (A, error), the context R becomes the last
// argument to be applied, even though it appears first in the original function signature.
// This is intentional and follows Go's context-first convention while enabling functional
// composition patterns.
//
// Why R is the last curried argument:
//
// - In Go, context conventionally comes first: func(ctx Context, params...) (Result, error)
// - In curried form: Curry2(f)(param1)(param2) returns ReaderResult[R, A]
// - The ReaderResult is then applied to R: Curry2(f)(param1)(param2)(ctx)
// - This allows partial application of business parameters before providing the context/object
//
// Object-Oriented Example:
//
// // A service struct that acts as the Reader context
// type UserService struct {
// db *sql.DB
// cache Cache
// }
//
// // A method-like function following Go conventions (context first)
// func (s *UserService) GetUserByID(ctx context.Context, id int) (User, error) {
// // Use s.db and s.cache...
// }
//
// func (s *UserService) UpdateUser(ctx context.Context, id int, name string) (User, error) {
// // Use s.db and s.cache...
// }
//
// // Curry these into composable operations
// getUser := readerresult.Curry1((*UserService).GetUserByID)
// updateUser := readerresult.Curry2((*UserService).UpdateUser)
//
// // Now compose operations that will be bound to a UserService instance
// type Context struct {
// Svc *UserService
// }
//
// pipeline := F.Pipe2(
// getUser(42), // ReaderResult[Context, User]
// readerresult.Chain(func(user User) readerresult.ReaderResult[Context, User] {
// newName := user.Name + " (updated)"
// return updateUser(user.ID)(newName)
// }),
// )
//
// // Execute by providing the service instance as context
// svc := &UserService{db: db, cache: cache}
// ctx := Context{Svc: svc}
// updatedUser, err := pipeline(ctx)
//
// The key insight is that currying creates a chain where:
// 1. Business parameters are applied first: getUser(42)
// 2. This returns a ReaderResult that waits for the context
// 3. Multiple operations can be composed before providing the context
// 4. Finally, the context/object is provided to execute everything: pipeline(ctx)
//
// This pattern is particularly useful for:
// - Creating reusable operation pipelines independent of service instances
// - Testing with mock service instances
// - Dependency injection in a functional style
// - Composing operations that share the same service context
//
// # Error Handling
//
// ReaderResult provides several functions for error handling:
//
// - Left/Right - create failed/successful values
// - GetOrElse - provide a default value for errors
// - OrElse - recover from errors with an alternative computation
// - Fold - handle both success and failure cases
// - ChainEitherK - lift result.Result computations into ReaderResult
//
// # Relationship to Other Monads
//
// ReaderResult is related to several other monads in this library:
//
// - Reader[R, A] - ReaderResult without error handling
// - Result[A] (Either[error, A]) - error handling without environment
// - ReaderEither[R, E, A] - like ReaderResult but with custom error type E
// - IOResult[A] - like ReaderResult but with no environment (IO with errors)
//
// # Performance Note
//
// ReaderResult is a zero-cost abstraction - it compiles to a simple function type
// with no runtime overhead beyond the underlying computation.
package readerresult

View File

@@ -0,0 +1,107 @@
// 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/idiomatic/readerresult"
"github.com/IBM/fp-go/v2/reader"
)
// SequenceReader swaps the order of nested environment parameters when the inner type is a Reader.
//
// It transforms ReaderResult[Reader[R, A]] into a function that takes context.Context first,
// then R, and returns (A, error). This is useful when you have a ReaderResult computation
// that produces a Reader, and you want to sequence the environment dependencies.
//
// Type Parameters:
// - R: The inner Reader's environment type
// - A: The final result type
//
// Parameters:
// - ma: A ReaderResult that produces a Reader[R, A]
//
// Returns:
// - A Kleisli arrow that takes context.Context and R to produce (A, error)
//
// Example:
//
// type Config struct {
// DatabaseURL string
// }
//
// // Returns a ReaderResult that produces a Reader
// getDBReader := func(ctx context.Context) (reader.Reader[Config, string], error) {
// return func(cfg Config) string {
// return cfg.DatabaseURL
// }, nil
// }
//
// // Sequence the environments: context.Context -> Config -> string
// sequenced := readerresult.SequenceReader[Config, string](getDBReader)
// result, err := sequenced(ctx)(config)
//
//go:inline
func SequenceReader[R, A any](ma ReaderResult[Reader[R, A]]) RR.Kleisli[context.Context, R, A] {
return RR.SequenceReader(ma)
}
// TraverseReader combines SequenceReader with a Kleisli arrow transformation.
//
// It takes a Reader Kleisli arrow (a function from A to Reader[R, B]) and returns
// a function that transforms ReaderResult[A] into a Kleisli arrow from context.Context
// and R to B. This is useful for transforming values within a ReaderResult while
// introducing an additional Reader dependency.
//
// Type Parameters:
// - R: The Reader's environment type
// - A: The input type
// - B: The output type
//
// Parameters:
// - f: A Kleisli arrow that transforms A into Reader[R, B]
//
// Returns:
// - A function that transforms ReaderResult[A] into a Kleisli arrow from context.Context and R to B
//
// Example:
//
// type Config struct {
// Multiplier int
// }
//
// // A Kleisli arrow that uses Config to transform int to string
// formatWithConfig := func(n int) reader.Reader[Config, string] {
// return func(cfg Config) string {
// return fmt.Sprintf("Value: %d", n * cfg.Multiplier)
// }
// }
//
// // Create a ReaderResult[int]
// getValue := readerresult.Of[int](42)
//
// // Traverse: transform the int using the Reader Kleisli arrow
// traversed := readerresult.TraverseReader[Config](formatWithConfig)(getValue)
// result, err := traversed(ctx)(Config{Multiplier: 2})
// // result == "Value: 84"
//
//go:inline
func TraverseReader[R, A, B any](
f reader.Kleisli[R, A, B],
) func(ReaderResult[A]) RR.Kleisli[context.Context, R, B] {
return RR.TraverseReader[context.Context](f)
}

View File

@@ -0,0 +1,134 @@
// 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/idiomatic/readerresult"
)
// From0 converts a context-taking function into a thunk that returns a ReaderResult.
//
// Unlike Curry0 which returns a ReaderResult directly, From0 returns a function
// that when called produces a ReaderResult. This is useful for lazy evaluation.
//
// Type Parameters:
// - A: The return value type
//
// Parameters:
// - f: A function that takes context.Context and returns (A, error)
//
// Returns:
// - A thunk (function with no parameters) that returns ReaderResult[A]
//
// Example:
//
// func getConfig(ctx context.Context) (Config, error) {
// return Config{Port: 8080}, nil
// }
// thunk := readerresult.From0(getConfig)
// rr := thunk() // Create the ReaderResult
// config, err := rr(ctx) // Execute it
//
//go:inline
func From0[A any](f func(context.Context) (A, error)) func() ReaderResult[A] {
return RR.From0(f)
}
// From1 converts a function with one parameter into an uncurried ReaderResult-returning function.
//
// Unlike Curry1 which returns a curried function, From1 returns a function that takes
// all parameters at once (except context). This is more convenient for direct calls.
//
// Type Parameters:
// - T1: The parameter type
// - A: The return value type
//
// Parameters:
// - f: A function that takes (context.Context, T1) and returns (A, error)
//
// Returns:
// - A function that takes T1 and returns ReaderResult[A]
//
// Example:
//
// func getUser(ctx context.Context, id int) (User, error) {
// return User{ID: id}, nil
// }
// getUserRR := readerresult.From1(getUser)
// rr := getUserRR(42)
// user, err := rr(ctx)
//
//go:inline
func From1[T1, A any](f func(context.Context, T1) (A, error)) func(T1) ReaderResult[A] {
return RR.From1(f)
}
// From2 converts a function with two parameters into an uncurried ReaderResult-returning function.
//
// Type Parameters:
// - T1: The first parameter type
// - T2: The second parameter type
// - A: The return value type
//
// Parameters:
// - f: A function that takes (context.Context, T1, T2) and returns (A, error)
//
// Returns:
// - A function that takes (T1, T2) and returns ReaderResult[A]
//
// Example:
//
// func updateUser(ctx context.Context, id int, name string) (User, error) {
// return User{ID: id, Name: name}, nil
// }
// updateUserRR := readerresult.From2(updateUser)
// rr := updateUserRR(42, "Alice")
// user, err := rr(ctx)
//
//go:inline
func From2[T1, T2, A any](f func(context.Context, T1, T2) (A, error)) func(T1, T2) ReaderResult[A] {
return RR.From2(f)
}
// From3 converts a function with three parameters into an uncurried ReaderResult-returning function.
//
// Type Parameters:
// - T1: The first parameter type
// - T2: The second parameter type
// - T3: The third parameter type
// - A: The return value type
//
// Parameters:
// - f: A function that takes (context.Context, T1, T2, T3) and returns (A, error)
//
// Returns:
// - A function that takes (T1, T2, T3) and returns ReaderResult[A]
//
// Example:
//
// func createPost(ctx context.Context, userID int, title, body string) (Post, error) {
// return Post{UserID: userID, Title: title, Body: body}, nil
// }
// createPostRR := readerresult.From3(createPost)
// rr := createPostRR(42, "Title", "Body")
// post, err := rr(ctx)
//
//go:inline
func From3[T1, T2, T3, A any](f func(context.Context, T1, T2, T3) (A, error)) func(T1, T2, T3) ReaderResult[A] {
return RR.From3(f)
}

View File

@@ -0,0 +1,120 @@
// 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/idiomatic/readerresult"
M "github.com/IBM/fp-go/v2/monoid"
)
// AlternativeMonoid creates a Monoid for ReaderResult using the Alternative semantics.
//
// The Alternative semantics means that the monoid operation tries the first computation,
// and if it fails, tries the second one. The empty element is a computation that always fails.
// The inner values are combined using the provided monoid when both computations succeed.
//
// Type Parameters:
// - A: The value type
//
// Parameters:
// - m: A Monoid[A] for combining successful values
//
// Returns:
// - A Monoid[ReaderResult[A]] with Alternative semantics
//
// Example:
//
// import "github.com/IBM/fp-go/v2/monoid"
//
// // Monoid for integers with addition
// intMonoid := monoid.MonoidSum[int]()
// rrMonoid := readerresult.AlternativeMonoid(intMonoid)
//
// rr1 := readerresult.Right(10)
// rr2 := readerresult.Right(20)
// combined := rrMonoid.Concat(rr1, rr2)
// value, err := combined(ctx) // Returns (30, nil)
//
//go:inline
func AlternativeMonoid[A any](m M.Monoid[A]) Monoid[A] {
return RR.AlternativeMonoid[context.Context](m)
}
// AltMonoid creates a Monoid for ReaderResult using Alt semantics with a custom zero.
//
// The Alt semantics means that the monoid operation tries the first computation,
// and if it fails, tries the second one. The provided zero is used as the empty element.
//
// Type Parameters:
// - A: The value type
//
// Parameters:
// - zero: A lazy ReaderResult[A] to use as the empty element
//
// Returns:
// - A Monoid[ReaderResult[A]] with Alt semantics
//
// Example:
//
// zero := func() readerresult.ReaderResult[int] {
// return readerresult.Left[int](errors.New("empty"))
// }
// rrMonoid := readerresult.AltMonoid(zero)
//
// rr1 := readerresult.Left[int](errors.New("failed"))
// rr2 := readerresult.Right(42)
// combined := rrMonoid.Concat(rr1, rr2)
// value, err := combined(ctx) // Returns (42, nil) - uses second on first failure
//
//go:inline
func AltMonoid[A any](zero Lazy[ReaderResult[A]]) Monoid[A] {
return RR.AltMonoid(zero)
}
// ApplicativeMonoid creates a Monoid for ReaderResult using Applicative semantics.
//
// The Applicative semantics means that both computations are executed independently,
// and their results are combined using the provided monoid. If either fails, the
// entire operation fails.
//
// Type Parameters:
// - A: The value type
//
// Parameters:
// - m: A Monoid[A] for combining successful values
//
// Returns:
// - A Monoid[ReaderResult[A]] with Applicative semantics
//
// Example:
//
// import "github.com/IBM/fp-go/v2/monoid"
//
// // Monoid for integers with addition
// intMonoid := monoid.MonoidSum[int]()
// rrMonoid := readerresult.ApplicativeMonoid(intMonoid)
//
// rr1 := readerresult.Right(10)
// rr2 := readerresult.Right(20)
// combined := rrMonoid.Concat(rr1, rr2)
// value, err := combined(ctx) // Returns (30, nil)
//
//go:inline
func ApplicativeMonoid[A any](m M.Monoid[A]) Monoid[A] {
return RR.ApplicativeMonoid[context.Context](m)
}

View File

@@ -0,0 +1,822 @@
// 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"
"sync"
"time"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/idiomatic/option"
RR "github.com/IBM/fp-go/v2/idiomatic/readerresult"
"github.com/IBM/fp-go/v2/idiomatic/result"
"github.com/IBM/fp-go/v2/reader"
RES "github.com/IBM/fp-go/v2/result"
)
// FromEither lifts a Result (Either[error, A]) into a ReaderResult.
//
// The resulting ReaderResult ignores the context.Context environment and simply
// returns the Result value. This is useful for converting existing Result values
// into the ReaderResult monad for composition with other ReaderResult operations.
//
// Type Parameters:
// - A: The success value type
//
// Parameters:
// - e: A Result[A] (Either[error, A]) to lift
//
// Returns:
// - A ReaderResult[A] that ignores the context and returns the Result
//
// Example:
//
// result := result.Of(42)
// rr := readerresult.FromEither(result)
// value, err := rr(ctx) // Returns (42, nil)
//
//go:inline
func FromEither[A any](e Result[A]) ReaderResult[A] {
return RR.FromEither[context.Context](e)
}
// FromResult creates a ReaderResult from a Go-style (value, error) tuple.
//
// This is a convenience function for converting standard Go error handling
// into the ReaderResult monad. The resulting ReaderResult ignores the context.
//
// Type Parameters:
// - A: The value type
//
// Parameters:
// - a: The value
// - err: The error (nil for success)
//
// Returns:
// - A ReaderResult[A] that returns the given value and error
//
// Example:
//
// rr := readerresult.FromResult(42, nil)
// value, err := rr(ctx) // Returns (42, nil)
//
// rr2 := readerresult.FromResult(0, errors.New("failed"))
// value, err := rr2(ctx) // Returns (0, error)
//
//go:inline
func FromResult[A any](a A, err error) ReaderResult[A] {
return RR.FromResult[context.Context](a, err)
}
//go:inline
func RightReader[A any](rdr Reader[context.Context, A]) ReaderResult[A] {
return RR.RightReader(rdr)
}
//go:inline
func LeftReader[A, R any](l Reader[context.Context, error]) ReaderResult[A] {
return RR.LeftReader[A](l)
}
// Left creates a ReaderResult that always fails with the given error.
//
// This is the error constructor for ReaderResult, analogous to Either's Left.
// The resulting computation ignores the context and immediately returns the error.
//
// Type Parameters:
// - A: The success type (for type inference)
//
// Parameters:
// - err: The error to return
//
// Returns:
// - A ReaderResult[A] that always fails with the given error
//
// Example:
//
// rr := readerresult.Left[int](errors.New("failed"))
// value, err := rr(ctx) // Returns (0, error)
//
//go:inline
func Left[A any](err error) ReaderResult[A] {
return RR.Left[context.Context, A](err)
}
// Right creates a ReaderResult that always succeeds with the given value.
//
// This is the success constructor for ReaderResult, analogous to Either's Right.
// The resulting computation ignores the context and immediately returns the value.
//
// Type Parameters:
// - A: The value type
//
// Parameters:
// - a: The value to return
//
// Returns:
// - A ReaderResult[A] that always succeeds with the given value
//
// Example:
//
// rr := readerresult.Right(42)
// value, err := rr(ctx) // Returns (42, nil)
//
//go:inline
func Right[A any](a A) ReaderResult[A] {
return RR.Right[context.Context, A](a)
}
// FromReader lifts a Reader into a ReaderResult that always succeeds.
//
// The Reader computation is executed and its result is wrapped in a successful Result.
// This is useful for incorporating Reader computations into ReaderResult pipelines.
//
// Type Parameters:
// - A: The value type
//
// Parameters:
// - r: A Reader[context.Context, A] to lift
//
// Returns:
// - A ReaderResult[A] that executes the Reader and always succeeds
//
// Example:
//
// getConfig := func(ctx context.Context) Config {
// return Config{Port: 8080}
// }
// rr := readerresult.FromReader(getConfig)
// value, err := rr(ctx) // Returns (Config{Port: 8080}, nil)
//
//go:inline
func FromReader[A any](r Reader[context.Context, A]) ReaderResult[A] {
return RR.FromReader(r)
}
// MonadMap transforms the success value of a ReaderResult using the given function.
//
// If the ReaderResult fails, the error is propagated unchanged. This is the
// Functor's map operation for ReaderResult.
//
// Type Parameters:
// - A: The input value type
// - B: The output value type
//
// Parameters:
// - fa: The ReaderResult to transform
// - f: The transformation function
//
// Returns:
// - A ReaderResult[B] with the transformed value
//
// Example:
//
// rr := readerresult.Right(42)
// mapped := readerresult.MonadMap(rr, func(n int) string {
// return fmt.Sprintf("Value: %d", n)
// })
// value, err := mapped(ctx) // Returns ("Value: 42", nil)
//
//go:inline
func MonadMap[A, B any](fa ReaderResult[A], f func(A) B) ReaderResult[B] {
return RR.MonadMap(fa, f)
}
// Map is the curried version of MonadMap, useful for function composition.
//
// It returns an Operator that can be used in pipelines with F.Pipe.
//
// Type Parameters:
// - A: The input value type
// - B: The output value type
//
// Parameters:
// - f: The transformation function
//
// Returns:
// - An Operator that transforms ReaderResult[A] to ReaderResult[B]
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// rr := readerresult.Right(42)
// result := F.Pipe1(
// rr,
// readerresult.Map(func(n int) string {
// return fmt.Sprintf("Value: %d", n)
// }),
// )
//
//go:inline
func Map[A, B any](f func(A) B) Operator[A, B] {
return RR.Map[context.Context](f)
}
// MonadChain sequences two ReaderResult computations where the second depends on the first.
//
// This is the monadic bind operation (flatMap). If the first computation fails,
// the error is propagated and the second computation is not executed. Both
// computations share the same context.Context environment.
//
// Type Parameters:
// - A: The input value type
// - B: The output value type
//
// Parameters:
// - ma: The first ReaderResult computation
// - f: A Kleisli arrow that produces the second computation based on the first's result
//
// Returns:
// - A ReaderResult[B] representing the sequenced computation
//
// Example:
//
// getUser := readerresult.Right(User{ID: 1, Name: "Alice"})
// getPosts := func(user User) readerresult.ReaderResult[[]Post] {
// return readerresult.Right([]Post{{UserID: user.ID}})
// }
// result := readerresult.MonadChain(getUser, getPosts)
// posts, err := result(ctx)
//
//go:inline
func MonadChain[A, B any](ma ReaderResult[A], f Kleisli[A, B]) ReaderResult[B] {
return RR.MonadChain(ma, f)
}
// Chain is the curried version of MonadChain, useful for function composition.
//
// It returns an Operator that can be used in pipelines with F.Pipe.
//
// Type Parameters:
// - A: The input value type
// - B: The output value type
//
// Parameters:
// - f: A Kleisli arrow for the second computation
//
// Returns:
// - An Operator that chains ReaderResult computations
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// result := F.Pipe1(
// getUser(1),
// readerresult.Chain(func(user User) readerresult.ReaderResult[[]Post] {
// return getPosts(user.ID)
// }),
// )
//
//go:inline
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
return RR.Chain(f)
}
// Of creates a ReaderResult that always succeeds with the given value.
//
// This is an alias for Right and represents the Applicative's pure/return operation.
// The resulting computation ignores the context and immediately returns the value.
//
// Type Parameters:
// - A: The value type
//
// Parameters:
// - a: The value to wrap
//
// Returns:
// - A ReaderResult[A] that always succeeds with the given value
//
// Example:
//
// rr := readerresult.Of(42)
// value, err := rr(ctx) // Returns (42, nil)
//
//go:inline
func Of[A any](a A) ReaderResult[A] {
return RR.Of[context.Context, A](a)
}
// MonadAp applies a function wrapped in a ReaderResult to a value wrapped in a ReaderResult.
//
// This is the Applicative's ap operation. Both computations are executed concurrently
// using goroutines, and the context is shared between them. If either computation fails,
// the entire operation fails. If the context is cancelled, the operation is aborted.
//
// The concurrent execution allows for parallel independent computations, which can
// improve performance when both operations involve I/O or other blocking operations.
//
// Type Parameters:
// - B: The result type after applying the function
// - A: The input type to the function
//
// Parameters:
// - fab: A ReaderResult containing a function from A to B
// - fa: A ReaderResult containing a value of type A
//
// Returns:
// - A ReaderResult[B] that applies the function to the value
//
// Example:
//
// // Create a function wrapped in ReaderResult
// addTen := readerresult.Right(func(n int) int {
// return n + 10
// })
//
// // Create a value wrapped in ReaderResult
// value := readerresult.Right(32)
//
// // Apply the function to the value
// result := readerresult.MonadAp(addTen, value)
// output, err := result(ctx) // Returns (42, nil)
//
// Error Handling:
//
// // If the function fails
// failedFn := readerresult.Left[func(int) int](errors.New("function error"))
// result := readerresult.MonadAp(failedFn, value)
// _, err := result(ctx) // Returns function error
//
// // If the value fails
// failedValue := readerresult.Left[int](errors.New("value error"))
// result := readerresult.MonadAp(addTen, failedValue)
// _, err := result(ctx) // Returns value error
//
// Context Cancellation:
//
// ctx, cancel := context.WithCancel(context.Background())
// cancel() // Cancel immediately
// result := readerresult.MonadAp(addTen, value)
// _, err := result(ctx) // Returns context cancellation error
func MonadAp[B, A any](fab ReaderResult[func(A) B], fa ReaderResult[A]) ReaderResult[B] {
return func(ctx context.Context) (B, error) {
if ctx.Err() != nil {
return result.Left[B](context.Cause(ctx))
}
var wg sync.WaitGroup
wg.Add(1)
cancelCtx, cancelFct := context.WithCancel(ctx)
defer cancelFct()
var a A
var aerr error
go func() {
defer wg.Done()
a, aerr = fa(cancelCtx)
if aerr != nil {
cancelFct()
}
}()
ab, aberr := fab(cancelCtx)
if aberr != nil {
cancelFct()
wg.Wait()
return result.Left[B](aberr)
}
wg.Wait()
if aerr != nil {
return result.Left[B](aerr)
}
return result.Of(ab(a))
}
}
// Ap is the curried version of MonadAp, useful for function composition.
//
// It fixes the value argument and returns an Operator that can be applied
// to a ReaderResult containing a function. This is particularly useful in
// pipelines where you want to apply a fixed value to various functions.
//
// Type Parameters:
// - B: The result type after applying the function
// - A: The input type to the function
//
// Parameters:
// - fa: A ReaderResult containing a value of type A
//
// Returns:
// - An Operator that applies the value to a function wrapped in ReaderResult
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// value := readerresult.Right(32)
// addTen := readerresult.Right(func(n int) int { return n + 10 })
//
// result := F.Pipe1(
// addTen,
// readerresult.Ap[int](value),
// )
// output, err := result(ctx) // Returns (42, nil)
//
//go:inline
func Ap[B, A any](fa ReaderResult[A]) Operator[func(A) B, B] {
return function.Bind2nd(MonadAp[B, A], fa)
}
//go:inline
func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) Kleisli[A, A] {
return RR.FromPredicate[context.Context](pred, onFalse)
}
//go:inline
func Fold[A, B any](onLeft reader.Kleisli[context.Context, error, B], onRight reader.Kleisli[context.Context, A, B]) func(ReaderResult[A]) Reader[context.Context, B] {
return RR.Fold(onLeft, onRight)
}
//go:inline
func GetOrElse[A any](onLeft reader.Kleisli[context.Context, error, A]) func(ReaderResult[A]) Reader[context.Context, A] {
return RR.GetOrElse(onLeft)
}
//go:inline
func OrElse[A any](onLeft Kleisli[error, A]) Operator[A, A] {
return RR.OrElse(onLeft)
}
//go:inline
func OrLeft[A any](onLeft reader.Kleisli[context.Context, error, error]) Operator[A, A] {
return RR.OrLeft[A](onLeft)
}
// Ask retrieves the current context.Context environment.
//
// This is the Reader's ask operation, which provides access to the environment.
// It always succeeds and returns the context that was passed in.
//
// Returns:
// - A ReaderResult[context.Context] that returns the environment
//
// Example:
//
// rr := readerresult.Ask()
// ctx, err := rr(context.Background()) // Returns (context.Background(), nil)
//
//go:inline
func Ask() ReaderResult[context.Context] {
return RR.Ask[context.Context]()
}
// Asks extracts a value from the context.Context environment using a Reader function.
//
// This is useful for accessing specific parts of the environment. The Reader
// function is applied to the context, and the result is wrapped in a successful ReaderResult.
//
// Type Parameters:
// - A: The extracted value type
//
// Parameters:
// - r: A Reader function that extracts a value from the context
//
// Returns:
// - A ReaderResult[A] that extracts and returns the value
//
// Example:
//
// type key int
// const userKey key = 0
//
// getUser := readerresult.Asks(func(ctx context.Context) User {
// return ctx.Value(userKey).(User)
// })
// user, err := getUser(ctx)
//
//go:inline
func Asks[A any](r Reader[context.Context, A]) ReaderResult[A] {
return RR.Asks(r)
}
//go:inline
func MonadChainEitherK[A, B any](ma ReaderResult[A], f RES.Kleisli[A, B]) ReaderResult[B] {
return RR.MonadChainEitherK[context.Context, A, B](ma, f)
}
//go:inline
func ChainEitherK[A, B any](f RES.Kleisli[A, B]) Operator[A, B] {
return RR.ChainEitherK[context.Context, A, B](f)
}
//go:inline
func MonadChainReaderK[A, B any](ma ReaderResult[A], f result.Kleisli[A, B]) ReaderResult[B] {
return RR.MonadChainReaderK(ma, f)
}
//go:inline
func ChainReaderK[A, B any](f result.Kleisli[A, B]) Operator[A, B] {
return RR.ChainReaderK[context.Context](f)
}
//go:inline
func ChainOptionK[A, B any](onNone Lazy[error]) func(option.Kleisli[A, B]) Operator[A, B] {
return RR.ChainOptionK[context.Context, A, B](onNone)
}
// Flatten removes one level of ReaderResult nesting.
//
// This is equivalent to Chain with the identity function. It's useful when you have
// a ReaderResult that produces another ReaderResult and want to collapse them into one.
//
// Type Parameters:
// - A: The inner value type
//
// Parameters:
// - mma: A nested ReaderResult[ReaderResult[A]]
//
// Returns:
// - A flattened ReaderResult[A]
//
// Example:
//
// nested := readerresult.Right(readerresult.Right(42))
// flattened := readerresult.Flatten(nested)
// value, err := flattened(ctx) // Returns (42, nil)
//
//go:inline
func Flatten[A any](mma ReaderResult[ReaderResult[A]]) ReaderResult[A] {
return RR.Flatten(mma)
}
//go:inline
func MonadBiMap[A, B any](fa ReaderResult[A], f Endomorphism[error], g func(A) B) ReaderResult[B] {
return RR.MonadBiMap(fa, f, g)
}
//go:inline
func BiMap[A, B any](f Endomorphism[error], g func(A) B) Operator[A, B] {
return RR.BiMap[context.Context](f, g)
}
// Read executes a ReaderResult by providing it with a context.Context.
//
// This is the elimination form for ReaderResult - it "runs" the computation
// by supplying the required environment, producing a (value, error) tuple.
//
// Type Parameters:
// - A: The result value type
//
// Parameters:
// - ctx: The context.Context environment to provide
//
// Returns:
// - A function that executes a ReaderResult[A] and returns (A, error)
//
// Example:
//
// rr := readerresult.Right(42)
// execute := readerresult.Read[int](ctx)
// value, err := execute(rr) // Returns (42, nil)
//
// // Or more commonly used directly:
// value, err := rr(ctx)
//
//go:inline
func Read[A any](ctx context.Context) func(ReaderResult[A]) (A, error) {
return RR.Read[A](ctx)
}
//go:inline
func MonadFlap[A, B any](fab ReaderResult[func(A) B], a A) ReaderResult[B] {
return RR.MonadFlap(fab, a)
}
//go:inline
func Flap[B, A any](a A) Operator[func(A) B, B] {
return RR.Flap[context.Context, B](a)
}
//go:inline
func MonadMapLeft[A any](fa ReaderResult[A], f Endomorphism[error]) ReaderResult[A] {
return RR.MonadMapLeft(fa, f)
}
//go:inline
func MapLeft[A any](f Endomorphism[error]) Operator[A, A] {
return RR.MapLeft[context.Context, A](f)
}
//go:inline
func MonadAlt[A any](first ReaderResult[A], second Lazy[ReaderResult[A]]) ReaderResult[A] {
return RR.MonadAlt(first, second)
}
//go:inline
func Alt[A any](second Lazy[ReaderResult[A]]) Operator[A, A] {
return RR.Alt(second)
}
// Local transforms the context.Context environment before passing it to a ReaderResult computation.
//
// This is the Reader's local operation, which allows you to modify the environment
// for a specific computation without affecting the outer context. The transformation
// function receives the current context and returns a new context along with a
// cancel function. The cancel function is automatically called when the computation
// completes (via defer), ensuring proper cleanup of resources.
//
// This is useful for:
// - Adding timeouts or deadlines to specific operations
// - Adding context values for nested computations
// - Creating isolated context scopes
// - Implementing context-based dependency injection
//
// Type Parameters:
// - A: The value type of the ReaderResult
//
// Parameters:
// - f: A function that transforms the context and returns a cancel function
//
// Returns:
// - An Operator that runs the computation with the transformed context
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// // Add a custom value to the context
// type key int
// const userKey key = 0
//
// addUser := readerresult.Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
// newCtx := context.WithValue(ctx, userKey, "Alice")
// return newCtx, func() {} // No-op cancel
// })
//
// getUser := readerresult.Asks(func(ctx context.Context) string {
// return ctx.Value(userKey).(string)
// })
//
// result := F.Pipe1(
// getUser,
// addUser,
// )
// user, err := result(context.Background()) // Returns ("Alice", nil)
//
// Timeout Example:
//
// // Add a 5-second timeout to a specific operation
// withTimeout := readerresult.Local[Data](func(ctx context.Context) (context.Context, context.CancelFunc) {
// return context.WithTimeout(ctx, 5*time.Second)
// })
//
// result := F.Pipe1(
// fetchData,
// withTimeout,
// )
func Local[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
return func(rr ReaderResult[A]) ReaderResult[A] {
return func(ctx context.Context) (A, error) {
if ctx.Err() != nil {
return result.Left[A](context.Cause(ctx))
}
otherCtx, otherCancel := f(ctx)
defer otherCancel()
return rr(otherCtx)
}
}
}
// WithTimeout adds a timeout to the context for a ReaderResult computation.
//
// This is a convenience wrapper around Local that uses context.WithTimeout.
// The computation must complete within the specified duration, or it will be
// cancelled. This is useful for ensuring operations don't run indefinitely
// and for implementing timeout-based error handling.
//
// The timeout is relative to when the ReaderResult is executed, not when
// WithTimeout is called. The cancel function is automatically called when
// the computation completes, ensuring proper cleanup.
//
// Type Parameters:
// - A: The value type of the ReaderResult
//
// Parameters:
// - timeout: The maximum duration for the computation
//
// Returns:
// - An Operator that runs the computation with a timeout
//
// Example:
//
// import (
// "time"
// F "github.com/IBM/fp-go/v2/function"
// )
//
// // Fetch data with a 5-second timeout
// fetchData := readerresult.FromReader(func(ctx context.Context) Data {
// // Simulate slow operation
// select {
// case <-time.After(10 * time.Second):
// return Data{Value: "slow"}
// case <-ctx.Done():
// return Data{}
// }
// })
//
// result := F.Pipe1(
// fetchData,
// readerresult.WithTimeout[Data](5*time.Second),
// )
// _, err := result(context.Background()) // Returns context.DeadlineExceeded after 5s
//
// Successful Example:
//
// quickFetch := readerresult.Right(Data{Value: "quick"})
// result := F.Pipe1(
// quickFetch,
// readerresult.WithTimeout[Data](5*time.Second),
// )
// data, err := result(context.Background()) // Returns (Data{Value: "quick"}, nil)
func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, timeout)
})
}
// WithDeadline adds an absolute deadline to the context for a ReaderResult computation.
//
// This is a convenience wrapper around Local that uses context.WithDeadline.
// The computation must complete before the specified time, or it will be
// cancelled. This is useful for coordinating operations that must finish
// by a specific time, such as request deadlines or scheduled tasks.
//
// The deadline is an absolute time, unlike WithTimeout which uses a relative
// duration. The cancel function is automatically called when the computation
// completes, ensuring proper cleanup.
//
// Type Parameters:
// - A: The value type of the ReaderResult
//
// Parameters:
// - deadline: The absolute time by which the computation must complete
//
// Returns:
// - An Operator that runs the computation with a deadline
//
// Example:
//
// import (
// "time"
// F "github.com/IBM/fp-go/v2/function"
// )
//
// // Operation must complete by 3 PM
// deadline := time.Date(2024, 1, 1, 15, 0, 0, 0, time.UTC)
//
// fetchData := readerresult.FromReader(func(ctx context.Context) Data {
// // Simulate operation
// select {
// case <-time.After(1 * time.Hour):
// return Data{Value: "done"}
// case <-ctx.Done():
// return Data{}
// }
// })
//
// result := F.Pipe1(
// fetchData,
// readerresult.WithDeadline[Data](deadline),
// )
// _, err := result(context.Background()) // Returns context.DeadlineExceeded if past deadline
//
// Combining with Parent Context:
//
// // If parent context already has a deadline, the earlier one takes precedence
// parentCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Hour))
// defer cancel()
//
// laterDeadline := time.Now().Add(2 * time.Hour)
// result := F.Pipe1(
// fetchData,
// readerresult.WithDeadline[Data](laterDeadline),
// )
// _, err := result(parentCtx) // Will use parent's 1-hour deadline
func WithDeadline[A any](deadline time.Time) Operator[A, A] {
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithDeadline(ctx, deadline)
})
}

View File

@@ -0,0 +1,952 @@
// 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"
"errors"
"fmt"
"strconv"
"testing"
"time"
N "github.com/IBM/fp-go/v2/number"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
// Helper types for testing
type User struct {
ID int
Name string
}
type Config struct {
Port int
DatabaseURL string
}
func TestFromEither(t *testing.T) {
ctx := context.Background()
t.Run("lifts successful Result", func(t *testing.T) {
// FromEither expects a Result[A] which is Either[error, A]
// We need to create it properly using the result package
rr := Right(42)
value, err := rr(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("lifts failing Result", func(t *testing.T) {
testErr := errors.New("test error")
rr := Left[int](testErr)
_, err := rr(ctx)
assert.Equal(t, testErr, err)
})
}
func TestFromResult(t *testing.T) {
ctx := context.Background()
t.Run("creates successful ReaderResult", func(t *testing.T) {
rr := FromResult(42, nil)
value, err := rr(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("creates failing ReaderResult", func(t *testing.T) {
testErr := errors.New("test error")
rr := FromResult(0, testErr)
_, err := rr(ctx)
assert.Equal(t, testErr, err)
})
}
func TestLeftAndRight(t *testing.T) {
ctx := context.Background()
t.Run("Right creates successful value", func(t *testing.T) {
rr := Right(42)
value, err := rr(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("Left creates error", func(t *testing.T) {
testErr := errors.New("test error")
rr := Left[int](testErr)
_, err := rr(ctx)
assert.Equal(t, testErr, err)
})
t.Run("Of is alias for Right", func(t *testing.T) {
rr := Of(42)
value, err := rr(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
}
func TestFromReader(t *testing.T) {
ctx := context.Background()
t.Run("lifts Reader as success", func(t *testing.T) {
r := func(ctx context.Context) int {
return 42
}
rr := FromReader(r)
value, err := rr(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("uses context", func(t *testing.T) {
type key int
const testKey key = 0
ctx := context.WithValue(context.Background(), testKey, 100)
r := func(ctx context.Context) int {
return ctx.Value(testKey).(int)
}
rr := FromReader(r)
value, err := rr(ctx)
assert.NoError(t, err)
assert.Equal(t, 100, value)
})
}
func TestMonadMap(t *testing.T) {
ctx := context.Background()
t.Run("transforms success value", func(t *testing.T) {
rr := Right(42)
mapped := MonadMap(rr, S.Format[int]("Value: %d"))
value, err := mapped(ctx)
assert.NoError(t, err)
assert.Equal(t, "Value: 42", value)
})
t.Run("propagates error", func(t *testing.T) {
testErr := errors.New("test error")
rr := Left[int](testErr)
mapped := MonadMap(rr, S.Format[int]("Value: %d"))
_, err := mapped(ctx)
assert.Equal(t, testErr, err)
})
t.Run("chains multiple maps", func(t *testing.T) {
rr := Right(10)
result := MonadMap(
MonadMap(rr, N.Mul(2)),
strconv.Itoa,
)
value, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, "20", value)
})
}
func TestMap(t *testing.T) {
ctx := context.Background()
t.Run("curried version works", func(t *testing.T) {
rr := Right(42)
mapper := Map(S.Format[int]("Value: %d"))
mapped := mapper(rr)
value, err := mapped(ctx)
assert.NoError(t, err)
assert.Equal(t, "Value: 42", value)
})
}
func TestMonadChain(t *testing.T) {
ctx := context.Background()
t.Run("sequences dependent computations", func(t *testing.T) {
getUser := Right(User{ID: 1, Name: "Alice"})
getPosts := func(user User) ReaderResult[string] {
return Right(fmt.Sprintf("Posts for %s", user.Name))
}
result := MonadChain(getUser, getPosts)
value, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, "Posts for Alice", value)
})
t.Run("propagates first error", func(t *testing.T) {
testErr := errors.New("first error")
getUser := Left[User](testErr)
getPosts := func(user User) ReaderResult[string] {
return Right("posts")
}
result := MonadChain(getUser, getPosts)
_, err := result(ctx)
assert.Equal(t, testErr, err)
})
t.Run("propagates second error", func(t *testing.T) {
testErr := errors.New("second error")
getUser := Right(User{ID: 1, Name: "Alice"})
getPosts := func(user User) ReaderResult[string] {
return Left[string](testErr)
}
result := MonadChain(getUser, getPosts)
_, err := result(ctx)
assert.Equal(t, testErr, err)
})
}
func TestChain(t *testing.T) {
ctx := context.Background()
t.Run("curried version works", func(t *testing.T) {
getUser := Right(User{ID: 1, Name: "Alice"})
chainer := Chain(func(user User) ReaderResult[string] {
return Right(fmt.Sprintf("Posts for %s", user.Name))
})
result := chainer(getUser)
value, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, "Posts for Alice", value)
})
}
func TestAsk(t *testing.T) {
t.Run("retrieves environment", func(t *testing.T) {
ctx := context.Background()
rr := Ask()
retrievedCtx, err := rr(ctx)
assert.NoError(t, err)
assert.Equal(t, ctx, retrievedCtx)
})
t.Run("always succeeds", func(t *testing.T) {
ctx := context.Background()
rr := Ask()
_, err := rr(ctx)
assert.NoError(t, err)
})
}
func TestAsks(t *testing.T) {
type key int
const userKey key = 0
t.Run("extracts value from environment", func(t *testing.T) {
user := User{ID: 1, Name: "Alice"}
ctx := context.WithValue(context.Background(), userKey, user)
getUser := Asks(func(ctx context.Context) User {
return ctx.Value(userKey).(User)
})
retrievedUser, err := getUser(ctx)
assert.NoError(t, err)
assert.Equal(t, user, retrievedUser)
})
t.Run("works with different extractors", func(t *testing.T) {
ctx := context.WithValue(context.Background(), userKey, 42)
getID := Asks(func(ctx context.Context) int {
return ctx.Value(userKey).(int)
})
id, err := getID(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, id)
})
}
func TestFlatten(t *testing.T) {
ctx := context.Background()
t.Run("removes one level of nesting", func(t *testing.T) {
nested := Right(Right(42))
flattened := Flatten(nested)
value, err := flattened(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("propagates outer error", func(t *testing.T) {
testErr := errors.New("outer error")
nested := Left[ReaderResult[int]](testErr)
flattened := Flatten(nested)
_, err := flattened(ctx)
assert.Equal(t, testErr, err)
})
t.Run("propagates inner error", func(t *testing.T) {
testErr := errors.New("inner error")
nested := Right(Left[int](testErr))
flattened := Flatten(nested)
_, err := flattened(ctx)
assert.Equal(t, testErr, err)
})
}
func TestRead(t *testing.T) {
t.Run("executes ReaderResult with context", func(t *testing.T) {
ctx := context.Background()
rr := Right(42)
execute := Read[int](ctx)
value, err := execute(rr)
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
}
func TestCurry0(t *testing.T) {
ctx := context.Background()
t.Run("converts function to ReaderResult", func(t *testing.T) {
f := func(ctx context.Context) (int, error) {
return 42, nil
}
rr := Curry0(f)
value, err := rr(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
}
func TestCurry1(t *testing.T) {
ctx := context.Background()
t.Run("curries function with one parameter", func(t *testing.T) {
f := func(ctx context.Context, id int) (User, error) {
return User{ID: id, Name: "Alice"}, nil
}
getUserRR := Curry1(f)
rr := getUserRR(1)
user, err := rr(ctx)
assert.NoError(t, err)
assert.Equal(t, User{ID: 1, Name: "Alice"}, user)
})
}
func TestCurry2(t *testing.T) {
ctx := context.Background()
t.Run("curries function with two parameters", func(t *testing.T) {
f := func(ctx context.Context, id int, name string) (User, error) {
return User{ID: id, Name: name}, nil
}
updateUserRR := Curry2(f)
rr := updateUserRR(1)("Bob")
user, err := rr(ctx)
assert.NoError(t, err)
assert.Equal(t, User{ID: 1, Name: "Bob"}, user)
})
}
func TestFrom1(t *testing.T) {
ctx := context.Background()
t.Run("converts function to uncurried form", func(t *testing.T) {
f := func(ctx context.Context, id int) (User, error) {
return User{ID: id, Name: "Alice"}, nil
}
getUserRR := From1(f)
rr := getUserRR(1)
user, err := rr(ctx)
assert.NoError(t, err)
assert.Equal(t, User{ID: 1, Name: "Alice"}, user)
})
}
// Note: SequenceReader and TraverseReader tests are complex due to type system interactions
// These functions are tested indirectly through their usage in other tests
func TestSequenceArray(t *testing.T) {
ctx := context.Background()
t.Run("sequences array of ReaderResults", func(t *testing.T) {
readers := []ReaderResult[int]{
Right(1),
Right(2),
Right(3),
}
result := SequenceArray(readers)
values, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, []int{1, 2, 3}, values)
})
t.Run("fails on first error", func(t *testing.T) {
testErr := errors.New("test error")
readers := []ReaderResult[int]{
Right(1),
Left[int](testErr),
Right(3),
}
result := SequenceArray(readers)
_, err := result(ctx)
assert.Equal(t, testErr, err)
})
}
func TestTraverseArray(t *testing.T) {
ctx := context.Background()
t.Run("applies function to each element", func(t *testing.T) {
double := func(n int) ReaderResult[int] {
return Right(n * 2)
}
numbers := []int{1, 2, 3}
result := TraverseArray(double)(numbers)
values, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, []int{2, 4, 6}, values)
})
}
func TestSequenceT2(t *testing.T) {
ctx := context.Background()
t.Run("combines two ReaderResults", func(t *testing.T) {
rr1 := Right(42)
rr2 := Right("hello")
result := SequenceT2(rr1, rr2)
tuple, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, tuple.F1)
assert.Equal(t, "hello", tuple.F2)
})
t.Run("fails if first fails", func(t *testing.T) {
testErr := errors.New("test error")
rr1 := Left[int](testErr)
rr2 := Right("hello")
result := SequenceT2(rr1, rr2)
_, err := result(ctx)
assert.Equal(t, testErr, err)
})
}
func TestDo(t *testing.T) {
ctx := context.Background()
t.Run("initializes do-notation", func(t *testing.T) {
type State struct {
Value int
}
result := Do(State{})
state, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, State{Value: 0}, state)
})
}
func TestBindTo(t *testing.T) {
ctx := context.Background()
t.Run("binds value to state", func(t *testing.T) {
type State struct {
User User
}
getUser := Right(User{ID: 1, Name: "Alice"})
result := BindTo(func(u User) State {
return State{User: u}
})(getUser)
state, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, User{ID: 1, Name: "Alice"}, state.User)
})
}
func TestMonadAp(t *testing.T) {
ctx := context.Background()
t.Run("applies function to value", func(t *testing.T) {
addTen := Right(N.Add(10))
value := Right(32)
result := MonadAp(addTen, value)
output, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, output)
})
t.Run("propagates function error", func(t *testing.T) {
testErr := errors.New("function error")
failedFn := Left[func(int) int](testErr)
value := Right(32)
result := MonadAp(failedFn, value)
_, err := result(ctx)
assert.Equal(t, testErr, err)
})
t.Run("propagates value error", func(t *testing.T) {
testErr := errors.New("value error")
addTen := Right(N.Add(10))
failedValue := Left[int](testErr)
result := MonadAp(addTen, failedValue)
_, err := result(ctx)
assert.Equal(t, testErr, err)
})
t.Run("handles context cancellation", func(t *testing.T) {
cancelCtx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
addTen := Right(N.Add(10))
value := Right(32)
result := MonadAp(addTen, value)
_, err := result(cancelCtx)
assert.Error(t, err)
assert.Equal(t, context.Canceled, err)
})
t.Run("works with different types", func(t *testing.T) {
toString := Right(func(n int) string {
return fmt.Sprintf("Number: %d", n)
})
value := Right(42)
result := MonadAp(toString, value)
output, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, "Number: 42", output)
})
t.Run("works with complex functions", func(t *testing.T) {
multiply := Right(func(user User) int {
return user.ID * 10
})
user := Right(User{ID: 5, Name: "Bob"})
result := MonadAp(multiply, user)
output, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, 50, output)
})
t.Run("executes both computations concurrently", func(t *testing.T) {
// This test verifies that both computations run concurrently
// by checking that they both complete even if one takes time
slowFn := func(ctx context.Context) (func(int) int, error) {
// Simulate some work
return N.Mul(2), nil
}
slowValue := func(ctx context.Context) (int, error) {
// Simulate some work
return 21, nil
}
result := MonadAp(slowFn, slowValue)
output, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, output)
})
}
func TestAp(t *testing.T) {
ctx := context.Background()
t.Run("curried version works", func(t *testing.T) {
value := Right(32)
addTen := Right(N.Add(10))
applyValue := Ap[int](value)
result := applyValue(addTen)
output, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, output)
})
t.Run("works in pipeline", func(t *testing.T) {
value := Right(32)
addTen := Right(N.Add(10))
// Using Ap in a functional pipeline style
result := Ap[int](value)(addTen)
output, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, output)
})
t.Run("propagates errors", func(t *testing.T) {
testErr := errors.New("value error")
failedValue := Left[int](testErr)
addTen := Right(N.Add(10))
result := Ap[int](failedValue)(addTen)
_, err := result(ctx)
assert.Equal(t, testErr, err)
})
}
func TestLocal(t *testing.T) {
t.Run("transforms context with custom value", func(t *testing.T) {
type key int
const userKey key = 0
// Create a computation that reads from context
getUser := Asks(func(ctx context.Context) string {
if user := ctx.Value(userKey); user != nil {
return user.(string)
}
return "unknown"
})
// Transform context to add user value
addUser := Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
newCtx := context.WithValue(ctx, userKey, "Alice")
return newCtx, func() {} // No-op cancel
})
// Apply transformation
result := addUser(getUser)
user, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, "Alice", user)
})
t.Run("cancel function is called", func(t *testing.T) {
cancelCalled := false
transform := Local[int](func(ctx context.Context) (context.Context, context.CancelFunc) {
return ctx, func() {
cancelCalled = true
}
})
rr := Right(42)
result := transform(rr)
_, err := result(context.Background())
assert.NoError(t, err)
assert.True(t, cancelCalled, "cancel function should be called")
})
t.Run("propagates errors", func(t *testing.T) {
testErr := errors.New("test error")
transform := Local[int](func(ctx context.Context) (context.Context, context.CancelFunc) {
return ctx, func() {}
})
rr := Left[int](testErr)
result := transform(rr)
_, err := result(context.Background())
assert.Equal(t, testErr, err)
})
t.Run("nested transformations", func(t *testing.T) {
type key int
const key1 key = 0
const key2 key = 1
getValues := Asks(func(ctx context.Context) string {
v1 := ctx.Value(key1).(string)
v2 := ctx.Value(key2).(string)
return v1 + ":" + v2
})
addFirst := Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithValue(ctx, key1, "A"), func() {}
})
addSecond := Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithValue(ctx, key2, "B"), func() {}
})
result := addSecond(addFirst(getValues))
value, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, "A:B", value)
})
t.Run("preserves parent context values", func(t *testing.T) {
type key int
const parentKey key = 0
const childKey key = 1
parentCtx := context.WithValue(context.Background(), parentKey, "parent")
getValues := Asks(func(ctx context.Context) string {
parent := ctx.Value(parentKey).(string)
child := ctx.Value(childKey).(string)
return parent + ":" + child
})
addChild := Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithValue(ctx, childKey, "child"), func() {}
})
result := addChild(getValues)
value, err := result(parentCtx)
assert.NoError(t, err)
assert.Equal(t, "parent:child", value)
})
}
func TestWithTimeout(t *testing.T) {
t.Run("completes within timeout", func(t *testing.T) {
rr := Right(42)
result := WithTimeout[int](1 * time.Second)(rr)
value, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("cancels on timeout", func(t *testing.T) {
// Create a computation that takes longer than timeout
slowComputation := func(ctx context.Context) (int, error) {
select {
case <-time.After(200 * time.Millisecond):
return 42, nil
case <-ctx.Done():
return 0, ctx.Err()
}
}
result := WithTimeout[int](50 * time.Millisecond)(slowComputation)
_, err := result(context.Background())
assert.Error(t, err)
assert.Equal(t, context.DeadlineExceeded, err)
})
t.Run("propagates errors", func(t *testing.T) {
testErr := errors.New("test error")
rr := Left[int](testErr)
result := WithTimeout[int](1 * time.Second)(rr)
_, err := result(context.Background())
assert.Equal(t, testErr, err)
})
t.Run("respects parent context timeout", func(t *testing.T) {
// Parent has shorter timeout
parentCtx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
slowComputation := func(ctx context.Context) (int, error) {
select {
case <-time.After(200 * time.Millisecond):
return 42, nil
case <-ctx.Done():
return 0, ctx.Err()
}
}
// Child has longer timeout, but parent's shorter timeout should win
result := WithTimeout[int](1 * time.Second)(slowComputation)
_, err := result(parentCtx)
assert.Error(t, err)
assert.Equal(t, context.DeadlineExceeded, err)
})
t.Run("works with context-aware operations", func(t *testing.T) {
type key int
const dataKey key = 0
ctx := context.WithValue(context.Background(), dataKey, "test-data")
getData := Asks(func(ctx context.Context) string {
return ctx.Value(dataKey).(string)
})
result := WithTimeout[string](1 * time.Second)(getData)
value, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, "test-data", value)
})
t.Run("multiple timeouts compose correctly", func(t *testing.T) {
rr := Right(42)
// Apply multiple timeouts - the shortest should win
result := WithTimeout[int](100 * time.Millisecond)(
WithTimeout[int](1 * time.Second)(rr),
)
value, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
}
func TestWithDeadline(t *testing.T) {
t.Run("completes before deadline", func(t *testing.T) {
deadline := time.Now().Add(1 * time.Second)
rr := Right(42)
result := WithDeadline[int](deadline)(rr)
value, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("cancels after deadline", func(t *testing.T) {
deadline := time.Now().Add(50 * time.Millisecond)
slowComputation := func(ctx context.Context) (int, error) {
select {
case <-time.After(200 * time.Millisecond):
return 42, nil
case <-ctx.Done():
return 0, ctx.Err()
}
}
result := WithDeadline[int](deadline)(slowComputation)
_, err := result(context.Background())
assert.Error(t, err)
assert.Equal(t, context.DeadlineExceeded, err)
})
t.Run("propagates errors", func(t *testing.T) {
testErr := errors.New("test error")
deadline := time.Now().Add(1 * time.Second)
rr := Left[int](testErr)
result := WithDeadline[int](deadline)(rr)
_, err := result(context.Background())
assert.Equal(t, testErr, err)
})
t.Run("respects parent context deadline", func(t *testing.T) {
// Parent has earlier deadline
parentDeadline := time.Now().Add(50 * time.Millisecond)
parentCtx, cancel := context.WithDeadline(context.Background(), parentDeadline)
defer cancel()
slowComputation := func(ctx context.Context) (int, error) {
select {
case <-time.After(200 * time.Millisecond):
return 42, nil
case <-ctx.Done():
return 0, ctx.Err()
}
}
// Child has later deadline, but parent's earlier deadline should win
childDeadline := time.Now().Add(1 * time.Second)
result := WithDeadline[int](childDeadline)(slowComputation)
_, err := result(parentCtx)
assert.Error(t, err)
assert.Equal(t, context.DeadlineExceeded, err)
})
t.Run("works with absolute time", func(t *testing.T) {
// Set deadline to a specific time in the future
deadline := time.Date(2130, 1, 1, 0, 0, 0, 0, time.UTC)
rr := Right(42)
result := WithDeadline[int](deadline)(rr)
value, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("handles past deadline", func(t *testing.T) {
// Deadline already passed - context will be immediately cancelled
deadline := time.Now().Add(-1 * time.Second)
// Use a computation that checks context cancellation
checkCtx := func(ctx context.Context) (int, error) {
if err := ctx.Err(); err != nil {
return 0, err
}
return 42, nil
}
result := WithDeadline[int](deadline)(checkCtx)
_, err := result(context.Background())
assert.Error(t, err)
assert.Equal(t, context.DeadlineExceeded, err)
})
t.Run("works with context values", func(t *testing.T) {
type key int
const configKey key = 0
ctx := context.WithValue(context.Background(), configKey, Config{Port: 8080})
deadline := time.Now().Add(1 * time.Second)
getConfig := Asks(func(ctx context.Context) Config {
return ctx.Value(configKey).(Config)
})
result := WithDeadline[Config](deadline)(getConfig)
config, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, Config{Port: 8080}, config)
})
}
func TestLocalWithTimeoutAndDeadline(t *testing.T) {
t.Run("combines Local with WithTimeout", func(t *testing.T) {
type key int
const userKey key = 0
addUser := Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithValue(ctx, userKey, "Alice"), func() {}
})
getUser := Asks(func(ctx context.Context) string {
return ctx.Value(userKey).(string)
})
result := WithTimeout[string](1 * time.Second)(addUser(getUser))
user, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, "Alice", user)
})
t.Run("combines Local with WithDeadline", func(t *testing.T) {
type key int
const dataKey key = 0
addData := Local[int](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithValue(ctx, dataKey, 42), func() {}
})
getData := Asks(func(ctx context.Context) int {
return ctx.Value(dataKey).(int)
})
deadline := time.Now().Add(1 * time.Second)
result := WithDeadline[int](deadline)(addData(getData))
value, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("complex composition", func(t *testing.T) {
type key int
const key1 key = 0
const key2 key = 1
// Add first value
addFirst := Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithValue(ctx, key1, "A"), func() {}
})
// Add second value
addSecond := Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithValue(ctx, key2, "B"), func() {}
})
// Read both values
getValues := Asks(func(ctx context.Context) string {
v1 := ctx.Value(key1).(string)
v2 := ctx.Value(key2).(string)
return v1 + ":" + v2
})
// Compose with timeout
result := WithTimeout[string](1 * time.Second)(
addSecond(addFirst(getValues)),
)
value, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, "A:B", value)
})
}

View File

@@ -0,0 +1,133 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerresult
import (
RR "github.com/IBM/fp-go/v2/idiomatic/readerresult"
T "github.com/IBM/fp-go/v2/tuple"
)
// SequenceT1 wraps a single ReaderResult in a Tuple1.
//
// This is mainly for consistency with the other SequenceT functions.
//
// Type Parameters:
// - A: The value type
//
// Parameters:
// - a: A ReaderResult[A]
//
// Returns:
// - A ReaderResult[Tuple1[A]]
//
// Example:
//
// rr := readerresult.Right(42)
// result := readerresult.SequenceT1(rr)
// tuple, err := result(ctx) // Returns (Tuple1{42}, nil)
//
//go:inline
func SequenceT1[A any](a ReaderResult[A]) ReaderResult[T.Tuple1[A]] {
return RR.SequenceT1(a)
}
// SequenceT2 combines two independent ReaderResult computations into a tuple.
//
// Both computations are executed with the same context. If either fails,
// the entire operation fails with the first error encountered.
//
// Type Parameters:
// - A: The first value type
// - B: The second value type
//
// Parameters:
// - a: The first ReaderResult
// - b: The second ReaderResult
//
// Returns:
// - A ReaderResult[Tuple2[A, B]] containing both results
//
// Example:
//
// getUser := readerresult.Right(User{ID: 1})
// getConfig := readerresult.Right(Config{Port: 8080})
// result := readerresult.SequenceT2(getUser, getConfig)
// tuple, err := result(ctx) // Returns (Tuple2{User, Config}, nil)
//
//go:inline
func SequenceT2[A, B any](
a ReaderResult[A],
b ReaderResult[B],
) ReaderResult[T.Tuple2[A, B]] {
return RR.SequenceT2(a, b)
}
// SequenceT3 combines three independent ReaderResult computations into a tuple.
//
// All computations are executed with the same context. If any fails,
// the entire operation fails with the first error encountered.
//
// Type Parameters:
// - A: The first value type
// - B: The second value type
// - C: The third value type
//
// Parameters:
// - a: The first ReaderResult
// - b: The second ReaderResult
// - c: The third ReaderResult
//
// Returns:
// - A ReaderResult[Tuple3[A, B, C]] containing all three results
//
//go:inline
func SequenceT3[A, B, C any](
a ReaderResult[A],
b ReaderResult[B],
c ReaderResult[C],
) ReaderResult[T.Tuple3[A, B, C]] {
return RR.SequenceT3(a, b, c)
}
// SequenceT4 combines four independent ReaderResult computations into a tuple.
//
// All computations are executed with the same context. If any fails,
// the entire operation fails with the first error encountered.
//
// Type Parameters:
// - A: The first value type
// - B: The second value type
// - C: The third value type
// - D: The fourth value type
//
// Parameters:
// - a: The first ReaderResult
// - b: The second ReaderResult
// - c: The third ReaderResult
// - d: The fourth ReaderResult
//
// Returns:
// - A ReaderResult[Tuple4[A, B, C, D]] containing all four results
//
//go:inline
func SequenceT4[A, B, C, D any](
a ReaderResult[A],
b ReaderResult[B],
c ReaderResult[C],
d ReaderResult[D],
) ReaderResult[T.Tuple4[A, B, C, D]] {
return RR.SequenceT4(a, b, c, d)
}

View File

@@ -0,0 +1,57 @@
// 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"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/endomorphism"
"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/reader"
"github.com/IBM/fp-go/v2/result"
)
type (
// Endomorphism represents a function from type A to type A.
Endomorphism[A any] = endomorphism.Endomorphism[A]
// Lazy represents a deferred computation that produces a value of type A when evaluated.
Lazy[A any] = lazy.Lazy[A]
// Option represents an optional value that may or may not be present.
Option[A any] = option.Option[A]
// Either represents a value that can be one of two types: Left (E) or Right (A).
Either[E, A any] = either.Either[E, A]
// Result represents an Either with error as the left type, compatible with Go's (value, error) tuple.
Result[A any] = result.Result[A]
// Reader represents a computation that depends on a read-only environment of type R and produces a value of type A.
Reader[R, A any] = reader.Reader[R, A]
ReaderResult[A any] = func(context.Context) (A, error)
// Monoid represents a monoid structure for ReaderResult values.
Monoid[A any] = monoid.Monoid[ReaderResult[A]]
Kleisli[A, B any] = Reader[A, ReaderResult[B]]
Operator[A, B any] = Kleisli[ReaderResult[A], B]
)

View File

@@ -0,0 +1,259 @@
// 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/idiomatic/ioresult"
"github.com/IBM/fp-go/v2/internal/readert"
"github.com/IBM/fp-go/v2/reader"
)
// Sequence swaps the order of nested environment parameters in a ReaderIOResult computation.
//
// This function transforms a computation that takes environment R2 and produces a ReaderIOResult[R1, A]
// into a Kleisli arrow that takes R1 first and returns a ReaderIOResult[R2, A].
//
// Type Parameters:
// - R1: The type of the inner environment (becomes the outer parameter after sequencing)
// - R2: The type of the outer environment (becomes the inner environment after sequencing)
// - A: The type of the value produced by the computation
//
// Parameters:
// - ma: A ReaderIOResult that depends on R2 and produces a ReaderIOResult[R1, A]
//
// Returns:
// - A Kleisli arrow (func(R1) func(R2) func() (A, error)) that reverses the environment order
//
// The transformation preserves error handling - if the outer computation fails, the error
// is propagated; if the inner computation fails, that error is also propagated.
//
// Example:
//
// type Database struct {
// ConnectionString string
// }
// type Config struct {
// Timeout int
// }
//
// // Original: takes Config, produces ReaderIOResult[Database, string]
// original := func(cfg Config) func() (func(Database) func() (string, error), error) {
// return func() (func(Database) func() (string, error), error) {
// if cfg.Timeout <= 0 {
// return nil, errors.New("invalid timeout")
// }
// return func(db Database) func() (string, error) {
// return func() (string, error) {
// if db.ConnectionString == "" {
// return "", errors.New("empty connection")
// }
// return fmt.Sprintf("Query on %s with timeout %d",
// db.ConnectionString, cfg.Timeout), nil
// }
// }, nil
// }
// }
//
// // Sequenced: takes Database first, then Config
// sequenced := Sequence(original)
// db := Database{ConnectionString: "localhost:5432"}
// cfg := Config{Timeout: 30}
// result, err := sequenced(db)(cfg)()
// // result: "Query on localhost:5432 with timeout 30"
func Sequence[R1, R2, A any](ma ReaderIOResult[R2, ReaderIOResult[R1, A]]) reader.Kleisli[R2, R1, IOResult[A]] {
return readert.Sequence(
ioresult.Chain,
ma,
)
}
// SequenceReader swaps the order of environment parameters when the inner computation is a pure Reader.
//
// This function is similar to Sequence but specialized for cases where the inner computation
// is a Reader (pure function) rather than a ReaderIOResult. It transforms a ReaderIOResult that
// produces a Reader into a Kleisli arrow with swapped environment order.
//
// Type Parameters:
// - R1: The type of the Reader's environment (becomes the outer parameter after sequencing)
// - R2: The type of the ReaderIOResult's environment (becomes the inner environment after sequencing)
// - A: The type of the value produced by the computation
//
// Parameters:
// - ma: A ReaderIOResult[R2, Reader[R1, A]] - depends on R2 and produces a pure Reader
//
// Returns:
// - A Kleisli arrow (func(R1) func(R2) func() (A, error)) that reverses the environment order
//
// The inner Reader computation is automatically lifted into the IOResult context (cannot fail).
// Only the outer ReaderIOResult can fail with an error.
//
// Example:
//
// type Config struct {
// Multiplier int
// }
//
// // Original: takes int, produces Reader[Config, int]
// original := func(x int) func() (func(Config) int, error) {
// return func() (func(Config) int, error) {
// if x < 0 {
// return nil, errors.New("negative value")
// }
// return func(cfg Config) int {
// return x * cfg.Multiplier
// }, nil
// }
// }
//
// // Sequenced: takes Config first, then int
// sequenced := SequenceReader(original)
// cfg := Config{Multiplier: 5}
// result, err := sequenced(cfg)(10)()
// // result: 50, err: nil
func SequenceReader[R1, R2, A any](ma ReaderIOResult[R2, Reader[R1, A]]) reader.Kleisli[R2, R1, IOResult[A]] {
return readert.SequenceReader(
ioresult.Map,
ma,
)
}
// Traverse transforms a ReaderIOResult computation by applying a Kleisli arrow that introduces
// a new environment dependency, effectively swapping the environment order.
//
// This is a higher-order function that takes a Kleisli arrow and returns a function that
// can transform ReaderIOResult computations. It's useful for introducing environment-dependent
// transformations into existing computations while reordering the environment parameters.
//
// Type Parameters:
// - R2: The type of the original computation's environment
// - R1: The type of the new environment introduced by the Kleisli arrow
// - A: The input type to the Kleisli arrow
// - B: The output type of the transformation
//
// Parameters:
// - f: A Kleisli arrow (func(A) ReaderIOResult[R1, B]) that transforms A to B with R1 dependency
//
// Returns:
// - A function that transforms ReaderIOResult[R2, A] into a Kleisli arrow with swapped environments
//
// The transformation preserves error handling from both the original computation and the
// Kleisli arrow. The resulting computation takes R1 first, then R2.
//
// Example:
//
// type Database struct {
// Prefix string
// }
//
// // Original computation: depends on int environment
// original := func(x int) func() (int, error) {
// return func() (int, error) {
// if x < 0 {
// return 0, errors.New("negative value")
// }
// return x * 2, nil
// }
// }
//
// // Kleisli arrow: transforms int to string with Database dependency
// format := func(value int) func(Database) func() (string, error) {
// return func(db Database) func() (string, error) {
// return func() (string, error) {
// return fmt.Sprintf("%s:%d", db.Prefix, value), nil
// }
// }
// }
//
// // Apply Traverse
// traversed := Traverse[int](format)
// result := traversed(original)
//
// // Use with Database first, then int
// db := Database{Prefix: "ID"}
// output, err := result(db)(10)()
// // output: "ID:20", err: nil
func Traverse[R2, R1, A, B any](
f Kleisli[R1, A, B],
) func(ReaderIOResult[R2, A]) Kleisli[R2, R1, B] {
return readert.Traverse[ReaderIOResult[R2, A]](
ioresult.Map,
ioresult.Chain,
f,
)
}
// TraverseReader transforms a ReaderIOResult computation by applying a Reader-based Kleisli arrow,
// introducing a new environment dependency while swapping the environment order.
//
// This function is similar to Traverse but specialized for pure Reader transformations that
// cannot fail. It's useful when you want to introduce environment-dependent logic without
// adding error handling complexity.
//
// Type Parameters:
// - R2: The type of the original computation's environment
// - R1: The type of the new environment introduced by the Reader Kleisli arrow
// - A: The input type to the Kleisli arrow
// - B: The output type of the transformation
//
// Parameters:
// - f: A Reader Kleisli arrow (func(A) func(R1) B) that transforms A to B with R1 dependency
//
// Returns:
// - A function that transforms ReaderIOResult[R2, A] into a Kleisli arrow with swapped environments
//
// The Reader transformation is automatically lifted into the IOResult context. Only the original
// ReaderIOResult computation can fail; the Reader transformation itself is pure and cannot fail.
//
// Example:
//
// type Config struct {
// Multiplier int
// }
//
// // Original computation: depends on int environment, may fail
// original := func(x int) func() (int, error) {
// return func() (int, error) {
// if x < 0 {
// return 0, errors.New("negative value")
// }
// return x * 2, nil
// }
// }
//
// // Pure Reader transformation: multiplies by config value
// multiply := func(value int) func(Config) int {
// return func(cfg Config) int {
// return value * cfg.Multiplier
// }
// }
//
// // Apply TraverseReader
// traversed := TraverseReader[int, Config](multiply)
// result := traversed(original)
//
// // Use with Config first, then int
// cfg := Config{Multiplier: 5}
// output, err := result(cfg)(10)()
// // output: 100 (10 * 2 * 5), err: nil
func TraverseReader[R2, R1, A, B any](
f reader.Kleisli[R1, A, B],
) func(ReaderIOResult[R2, A]) Kleisli[R2, R1, B] {
return readert.TraverseReader[ReaderIOResult[R2, A]](
ioresult.Map,
ioresult.Map,
f,
)
}

View File

@@ -0,0 +1,865 @@
// 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 (
"errors"
"fmt"
"testing"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/reader"
"github.com/stretchr/testify/assert"
)
func TestSequence(t *testing.T) {
t.Run("sequences parameter order for simple types", func(t *testing.T) {
// Original: takes int, returns ReaderIOResult[string, int]
original := func(x int) IOResult[ReaderIOResult[string, int]] {
return func() (ReaderIOResult[string, int], error) {
if x < 0 {
return nil, errors.New("negative value")
}
return func(s string) IOResult[int] {
return func() (int, error) {
return x + len(s), nil
}
}, nil
}
}
// Sequenced: takes string first, then int
sequenced := Sequence(original)
// Test original
innerFunc1, err1 := original(10)()
assert.NoError(t, err1)
result1, err2 := innerFunc1("hello")()
assert.NoError(t, err2)
assert.Equal(t, 15, result1)
// Test sequenced
result2, err3 := sequenced("hello")(10)()
assert.NoError(t, err3)
assert.Equal(t, 15, result2)
})
t.Run("preserves outer error", func(t *testing.T) {
expectedError := errors.New("outer error")
original := func(x int) IOResult[ReaderIOResult[string, int]] {
return func() (ReaderIOResult[string, int], error) {
if x < 0 {
return nil, expectedError
}
return func(s string) IOResult[int] {
return func() (int, error) {
return x + len(s), nil
}
}, nil
}
}
sequenced := Sequence(original)
// Test with error
_, err := sequenced("test")(-1)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("preserves inner error", func(t *testing.T) {
expectedError := errors.New("inner error")
original := func(x int) IOResult[ReaderIOResult[string, int]] {
return func() (ReaderIOResult[string, int], error) {
return func(s string) IOResult[int] {
return func() (int, error) {
if len(s) == 0 {
return 0, expectedError
}
return x + len(s), nil
}
}, nil
}
}
sequenced := Sequence(original)
// Test with inner error
_, err := sequenced("")(10)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("works with different types", func(t *testing.T) {
// Transform int to string
original := func(x int) IOResult[ReaderIOResult[string, string]] {
return func() (ReaderIOResult[string, string], error) {
return func(prefix string) IOResult[string] {
return func() (string, error) {
return fmt.Sprintf("%s-%d", prefix, x), nil
}
}, nil
}
}
sequenced := Sequence(original)
result, err := sequenced("ID")(42)()
assert.NoError(t, err)
assert.Equal(t, "ID-42", result)
})
t.Run("works with struct environments", func(t *testing.T) {
type Database struct {
ConnectionString string
}
type Config struct {
Timeout int
}
original := func(cfg Config) IOResult[ReaderIOResult[Database, string]] {
return func() (ReaderIOResult[Database, string], error) {
if cfg.Timeout <= 0 {
return nil, errors.New("invalid timeout")
}
return func(db Database) IOResult[string] {
return func() (string, error) {
if db.ConnectionString == "" {
return "", errors.New("empty connection string")
}
return fmt.Sprintf("Query on %s with timeout %d",
db.ConnectionString, cfg.Timeout), nil
}
}, nil
}
}
sequenced := Sequence(original)
db := Database{ConnectionString: "localhost:5432"}
cfg := Config{Timeout: 30}
result, err := sequenced(db)(cfg)()
assert.NoError(t, err)
assert.Equal(t, "Query on localhost:5432 with timeout 30", result)
})
t.Run("works with zero values", func(t *testing.T) {
original := func(x int) IOResult[ReaderIOResult[string, int]] {
return func() (ReaderIOResult[string, int], error) {
return func(s string) IOResult[int] {
return func() (int, error) {
return x + len(s), nil
}
}, nil
}
}
sequenced := Sequence(original)
result, err := sequenced("")(0)()
assert.NoError(t, err)
assert.Equal(t, 0, result)
})
t.Run("handles IO side effects correctly", func(t *testing.T) {
counter := 0
original := func(x int) IOResult[ReaderIOResult[string, int]] {
return func() (ReaderIOResult[string, int], error) {
counter++ // Side effect in outer IO
return func(s string) IOResult[int] {
return func() (int, error) {
counter++ // Side effect in inner IO
return x + len(s), nil
}
}, nil
}
}
sequenced := Sequence(original)
result, err := sequenced("test")(5)()
assert.NoError(t, err)
assert.Equal(t, 9, result)
assert.Equal(t, 2, counter) // Both side effects executed
})
}
func TestSequenceReader(t *testing.T) {
t.Run("sequences parameter order for Reader inner type", func(t *testing.T) {
// Original: takes int, returns Reader[string, int]
original := func(x int) IOResult[reader.Reader[string, int]] {
return func() (reader.Reader[string, int], error) {
if x < 0 {
return nil, errors.New("negative value")
}
return func(s string) int {
return x + len(s)
}, nil
}
}
// Sequenced: takes string first, then int
sequenced := SequenceReader(original)
// Test original
readerFunc, err1 := original(10)()
assert.NoError(t, err1)
value1 := readerFunc("hello")
assert.Equal(t, 15, value1)
// Test sequenced
value2, err2 := sequenced("hello")(10)()
assert.NoError(t, err2)
assert.Equal(t, 15, value2)
})
t.Run("preserves outer error", func(t *testing.T) {
expectedError := errors.New("outer error")
original := func(x int) IOResult[reader.Reader[string, int]] {
return func() (reader.Reader[string, int], error) {
if x < 0 {
return nil, expectedError
}
return func(s string) int {
return x + len(s)
}, nil
}
}
sequenced := SequenceReader(original)
// Test with error
_, err := sequenced("test")(-1)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("works with different types", func(t *testing.T) {
// Transform int to string using Reader
original := func(x int) IOResult[reader.Reader[string, string]] {
return func() (reader.Reader[string, string], error) {
return func(prefix string) string {
return fmt.Sprintf("%s-%d", prefix, x)
}, nil
}
}
sequenced := SequenceReader(original)
result, err := sequenced("ID")(42)()
assert.NoError(t, err)
assert.Equal(t, "ID-42", result)
})
t.Run("works with struct environments", func(t *testing.T) {
type Config struct {
Multiplier int
}
original := func(x int) IOResult[reader.Reader[Config, int]] {
return func() (reader.Reader[Config, int], error) {
if x < 0 {
return nil, errors.New("negative value")
}
return func(cfg Config) int {
return x * cfg.Multiplier
}, nil
}
}
sequenced := SequenceReader(original)
cfg := Config{Multiplier: 5}
result, err := sequenced(cfg)(10)()
assert.NoError(t, err)
assert.Equal(t, 50, result)
})
t.Run("works with zero values", func(t *testing.T) {
original := func(x int) IOResult[reader.Reader[string, int]] {
return func() (reader.Reader[string, int], error) {
return func(s string) int {
return x + len(s)
}, nil
}
}
sequenced := SequenceReader(original)
result, err := sequenced("")(0)()
assert.NoError(t, err)
assert.Equal(t, 0, result)
})
t.Run("handles IO side effects correctly", func(t *testing.T) {
counter := 0
original := func(x int) IOResult[reader.Reader[string, int]] {
return func() (reader.Reader[string, int], error) {
counter++ // Side effect in IO
return func(s string) int {
return x + len(s)
}, nil
}
}
sequenced := SequenceReader(original)
result, err := sequenced("test")(5)()
assert.NoError(t, err)
assert.Equal(t, 9, result)
assert.Equal(t, 1, counter) // Side effect executed
})
}
func TestTraverse(t *testing.T) {
t.Run("basic transformation with environment swap", func(t *testing.T) {
// Original: ReaderIOResult[int, int] - takes int environment, produces int
original := func(x int) IOResult[int] {
return func() (int, error) {
if x < 0 {
return 0, errors.New("negative value")
}
return x * 2, nil
}
}
// Kleisli function: func(int) ReaderIOResult[string, int]
kleisli := func(a int) ReaderIOResult[string, int] {
return func(s string) IOResult[int] {
return func() (int, error) {
return a + len(s), nil
}
}
}
// Traverse returns: func(ReaderIOResult[int, int]) func(string) ReaderIOResult[int, int]
traversed := Traverse[int](kleisli)
result := traversed(original)
// result is func(string) ReaderIOResult[int, int]
// Provide string first ("hello"), then int (10)
value, err := result("hello")(10)()
assert.NoError(t, err)
assert.Equal(t, 25, value) // (10 * 2) + len("hello") = 20 + 5 = 25
})
t.Run("preserves outer error", func(t *testing.T) {
expectedError := errors.New("outer error")
original := func(x int) IOResult[int] {
return func() (int, error) {
if x < 0 {
return 0, expectedError
}
return x, nil
}
}
kleisli := func(a int) ReaderIOResult[string, int] {
return func(s string) IOResult[int] {
return func() (int, error) {
return a + len(s), nil
}
}
}
traversed := Traverse[int](kleisli)
result := traversed(original)
// Test with negative value to trigger error
_, err := result("test")(-1)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("preserves inner error from Kleisli", func(t *testing.T) {
expectedError := errors.New("inner error")
original := Ask[int]()
kleisli := func(a int) ReaderIOResult[string, int] {
return func(s string) IOResult[int] {
return func() (int, error) {
if len(s) == 0 {
return 0, expectedError
}
return a + len(s), nil
}
}
}
traversed := Traverse[int](kleisli)
result := traversed(original)
// Test with empty string to trigger inner error
_, err := result("")(10)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("works with different types", func(t *testing.T) {
// Transform int to string using environment-dependent logic
original := Ask[int]()
kleisli := func(a int) ReaderIOResult[string, string] {
return func(prefix string) IOResult[string] {
return func() (string, error) {
return fmt.Sprintf("%s-%d", prefix, a), nil
}
}
}
traversed := Traverse[int](kleisli)
result := traversed(original)
value, err := result("ID")(42)()
assert.NoError(t, err)
assert.Equal(t, "ID-42", value)
})
t.Run("works with struct environments", func(t *testing.T) {
type Config struct {
Multiplier int
}
type Database struct {
Prefix string
}
original := func(cfg Config) IOResult[int] {
return func() (int, error) {
if cfg.Multiplier <= 0 {
return 0, errors.New("invalid multiplier")
}
return 10 * cfg.Multiplier, nil
}
}
kleisli := func(value int) ReaderIOResult[Database, string] {
return func(db Database) IOResult[string] {
return func() (string, error) {
return fmt.Sprintf("%s:%d", db.Prefix, value), nil
}
}
}
traversed := Traverse[Config](kleisli)
result := traversed(original)
cfg := Config{Multiplier: 5}
db := Database{Prefix: "result"}
value, err := result(db)(cfg)()
assert.NoError(t, err)
assert.Equal(t, "result:50", value)
})
t.Run("chains multiple transformations", func(t *testing.T) {
original := Ask[int]()
// First transformation: multiply by environment value
kleisli1 := func(a int) ReaderIOResult[int, int] {
return func(multiplier int) IOResult[int] {
return func() (int, error) {
return a * multiplier, nil
}
}
}
traversed := Traverse[int](kleisli1)
result := traversed(original)
value, err := result(3)(5)()
assert.NoError(t, err)
assert.Equal(t, 15, value) // 5 * 3 = 15
})
t.Run("works with zero values", func(t *testing.T) {
original := Ask[int]()
kleisli := func(a int) ReaderIOResult[string, int] {
return func(s string) IOResult[int] {
return func() (int, error) {
return a + len(s), nil
}
}
}
traversed := Traverse[int](kleisli)
result := traversed(original)
value, err := result("")(0)()
assert.NoError(t, err)
assert.Equal(t, 0, value)
})
t.Run("enables partial application", func(t *testing.T) {
original := Ask[int]()
kleisli := func(a int) ReaderIOResult[int, int] {
return func(factor int) IOResult[int] {
return func() (int, error) {
return a * factor, nil
}
}
}
traversed := Traverse[int](kleisli)
result := traversed(original)
// Partially apply factor
withFactor := result(3)
// Can now use with different inputs
value1, err1 := withFactor(10)()
assert.NoError(t, err1)
assert.Equal(t, 30, value1)
// Reuse with different input
value2, err2 := withFactor(20)()
assert.NoError(t, err2)
assert.Equal(t, 60, value2)
})
t.Run("handles IO side effects correctly", func(t *testing.T) {
counter := 0
original := func(x int) IOResult[int] {
return func() (int, error) {
counter++ // Side effect in outer IO
return x * 2, nil
}
}
kleisli := func(a int) ReaderIOResult[string, int] {
return func(s string) IOResult[int] {
return func() (int, error) {
counter++ // Side effect in inner IO
return a + len(s), nil
}
}
}
traversed := Traverse[int](kleisli)
result := traversed(original)
value, err := result("test")(5)()
assert.NoError(t, err)
assert.Equal(t, 14, value) // (5 * 2) + 4 = 14
assert.Equal(t, 2, counter) // Both side effects executed
})
}
func TestTraverseReader(t *testing.T) {
t.Run("basic transformation with Reader dependency", func(t *testing.T) {
type Config struct {
Multiplier int
}
// Original computation
original := F.Pipe1(
Ask[int](),
Map[int](N.Mul(2)),
)
// Reader-based transformation
multiply := func(a int) func(Config) int {
return func(cfg Config) int {
return a * cfg.Multiplier
}
}
// Apply TraverseReader
traversed := TraverseReader[int](multiply)
result := traversed(original)
// Provide Config first, then int
cfg := Config{Multiplier: 5}
value, err := result(cfg)(10)()
assert.NoError(t, err)
assert.Equal(t, 100, value) // (10 * 2) * 5 = 100
})
t.Run("preserves outer error", func(t *testing.T) {
type Config struct {
Multiplier int
}
expectedError := errors.New("outer error")
// Original computation that fails
original := func(x int) IOResult[int] {
return func() (int, error) {
if x < 0 {
return 0, expectedError
}
return x, nil
}
}
// Reader-based transformation (won't be called)
multiply := func(a int) func(Config) int {
return func(cfg Config) int {
return a * cfg.Multiplier
}
}
// Apply TraverseReader
traversed := TraverseReader[int](multiply)
result := traversed(original)
// Provide Config and negative value
cfg := Config{Multiplier: 5}
_, err := result(cfg)(-1)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("works with different types", func(t *testing.T) {
type Database struct {
Prefix string
}
// Original computation producing an int
original := Ask[int]()
// Reader-based transformation: int -> string using Database
format := func(a int) func(Database) string {
return func(db Database) string {
return fmt.Sprintf("%s:%d", db.Prefix, a)
}
}
// Apply TraverseReader
traversed := TraverseReader[int](format)
result := traversed(original)
// Provide Database first, then int
db := Database{Prefix: "ID"}
value, err := result(db)(42)()
assert.NoError(t, err)
assert.Equal(t, "ID:42", value)
})
t.Run("works with struct environments", func(t *testing.T) {
type Settings struct {
Prefix string
Suffix string
}
type Context struct {
Value int
}
// Original computation
original := func(ctx Context) IOResult[string] {
return func() (string, error) {
return fmt.Sprintf("value:%d", ctx.Value), nil
}
}
// Reader-based transformation using Settings
decorate := func(s string) func(Settings) string {
return func(settings Settings) string {
return settings.Prefix + s + settings.Suffix
}
}
// Apply TraverseReader
traversed := TraverseReader[Context](decorate)
result := traversed(original)
// Provide Settings first, then Context
settings := Settings{Prefix: "[", Suffix: "]"}
ctx := Context{Value: 100}
value, err := result(settings)(ctx)()
assert.NoError(t, err)
assert.Equal(t, "[value:100]", value)
})
t.Run("enables partial application", func(t *testing.T) {
type Config struct {
Factor int
}
// Original computation
original := Ask[int]()
// Reader-based transformation
scale := func(a int) func(Config) int {
return func(cfg Config) int {
return a * cfg.Factor
}
}
// Apply TraverseReader
traversed := TraverseReader[int](scale)
result := traversed(original)
// Partially apply Config
cfg := Config{Factor: 3}
withConfig := result(cfg)
// Can now use with different inputs
value1, err1 := withConfig(10)()
assert.NoError(t, err1)
assert.Equal(t, 30, value1)
// Reuse with different input
value2, err2 := withConfig(20)()
assert.NoError(t, err2)
assert.Equal(t, 60, value2)
})
t.Run("works with zero values", func(t *testing.T) {
type Config struct {
Offset int
}
// Original computation with zero value
original := Ask[int]()
// Reader-based transformation
add := func(a int) func(Config) int {
return func(cfg Config) int {
return a + cfg.Offset
}
}
// Apply TraverseReader
traversed := TraverseReader[int](add)
result := traversed(original)
// Provide Config with zero offset and zero input
cfg := Config{Offset: 0}
value, err := result(cfg)(0)()
assert.NoError(t, err)
assert.Equal(t, 0, value)
})
t.Run("chains multiple transformations", func(t *testing.T) {
type Config struct {
Multiplier int
}
// Original computation
original := func(x int) IOResult[int] {
return func() (int, error) {
return x * 2, nil
}
}
// Reader-based transformation
multiply := func(a int) func(Config) int {
return func(cfg Config) int {
return a * cfg.Multiplier
}
}
// Apply TraverseReader
traversed := TraverseReader[int](multiply)
result := traversed(original)
// Provide Config and execute
cfg := Config{Multiplier: 4}
value, err := result(cfg)(5)()
assert.NoError(t, err)
assert.Equal(t, 40, value) // (5 * 2) * 4 = 40
})
t.Run("works with complex Reader logic", func(t *testing.T) {
type ValidationRules struct {
MinValue int
MaxValue int
}
// Original computation
original := Ask[int]()
// Reader-based transformation with validation logic
validate := func(a int) func(ValidationRules) int {
return func(rules ValidationRules) int {
if a < rules.MinValue {
return rules.MinValue
}
if a > rules.MaxValue {
return rules.MaxValue
}
return a
}
}
// Apply TraverseReader
traversed := TraverseReader[int](validate)
result := traversed(original)
// Test with value within range
rules1 := ValidationRules{MinValue: 0, MaxValue: 100}
value1, err1 := result(rules1)(50)()
assert.NoError(t, err1)
assert.Equal(t, 50, value1)
// Test with value above max
rules2 := ValidationRules{MinValue: 0, MaxValue: 30}
value2, err2 := result(rules2)(50)()
assert.NoError(t, err2)
assert.Equal(t, 30, value2) // Clamped to max
// Test with value below min
rules3 := ValidationRules{MinValue: 60, MaxValue: 100}
value3, err3 := result(rules3)(50)()
assert.NoError(t, err3)
assert.Equal(t, 60, value3) // Clamped to min
})
t.Run("handles IO side effects correctly", func(t *testing.T) {
type Config struct {
Multiplier int
}
counter := 0
// Original computation with side effect
original := func(x int) IOResult[int] {
return func() (int, error) {
counter++ // Side effect
return x * 2, nil
}
}
// Reader-based transformation (pure, no side effects)
multiply := func(a int) func(Config) int {
return func(cfg Config) int {
return a * cfg.Multiplier
}
}
// Apply TraverseReader
traversed := TraverseReader[int](multiply)
result := traversed(original)
cfg := Config{Multiplier: 5}
value, err := result(cfg)(10)()
assert.NoError(t, err)
assert.Equal(t, 100, value)
assert.Equal(t, 1, counter) // Side effect executed once
})
}

View File

@@ -0,0 +1,987 @@
// 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/function"
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
"github.com/IBM/fp-go/v2/internal/chain"
"github.com/IBM/fp-go/v2/internal/fromio"
"github.com/IBM/fp-go/v2/internal/fromreader"
"github.com/IBM/fp-go/v2/internal/functor"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
)
// FromIOResult lifts an IOResult into a ReaderIOResult context.
// The resulting computation ignores the environment parameter and directly executes the IOResult.
//
// Type Parameters:
// - R: The type of the environment (ignored by the computation)
// - A: The type of the success value
//
// Parameters:
// - ma: The IOResult to lift
//
// Returns:
// - A ReaderIOResult that executes the IOResult regardless of the environment
//
// Example:
//
// ioResult := func() (int, error) { return 42, nil }
// readerIOResult := FromIOResult[Config](ioResult)
// result, err := readerIOResult(cfg)() // Returns 42, nil
//
//go:inline
func FromIOResult[R, A any](ma IOResult[A]) ReaderIOResult[R, A] {
return reader.Of[R](ma)
}
// RightIO lifts an IO computation into a ReaderIOResult as a successful value.
// The IO computation always succeeds, so it's wrapped in the Right (success) side.
//
// Type Parameters:
// - R: The type of the environment (ignored by the computation)
// - A: The type of the value produced by the IO
//
// Parameters:
// - ma: The IO computation to lift
//
// Returns:
// - A ReaderIOResult that executes the IO and wraps the result as a success
//
// Example:
//
// getCurrentTime := func() time.Time { return time.Now() }
// readerIOResult := RightIO[Config](getCurrentTime)
// result, err := readerIOResult(cfg)() // Returns current time, nil
func RightIO[R, A any](ma IO[A]) ReaderIOResult[R, A] {
return function.Pipe2(ma, ioresult.RightIO[A], FromIOResult[R, A])
}
// LeftIO lifts an IO computation that produces an error into a ReaderIOResult as a failure.
// The IO computation produces an error, which is wrapped in the Left (error) side.
//
// Type Parameters:
// - R: The type of the environment (ignored by the computation)
// - A: The type of the success value (never produced)
//
// Parameters:
// - ma: The IO computation that produces an error
//
// Returns:
// - A ReaderIOResult that executes the IO and wraps the error as a failure
//
// Example:
//
// getError := func() error { return errors.New("something went wrong") }
// readerIOResult := LeftIO[Config, int](getError)
// _, err := readerIOResult(cfg)() // Returns error
func LeftIO[R, A any](ma IO[error]) ReaderIOResult[R, A] {
return function.Pipe2(ma, ioresult.LeftIO[A], FromIOResult[R, A])
}
// FromIO lifts an IO computation into a ReaderIOResult context.
// This is an alias for RightIO - the IO computation always succeeds.
//
// Type Parameters:
// - R: The type of the environment (ignored by the computation)
// - E: Unused type parameter (kept for compatibility)
// - A: The type of the value produced by the IO
//
// Parameters:
// - ma: The IO computation to lift
//
// Returns:
// - A ReaderIOResult that executes the IO and wraps the result as a success
//
//go:inline
func FromIO[R, E, A any](ma IO[A]) ReaderIOResult[R, A] {
return RightIO[R](ma)
}
// FromReaderIO lifts a ReaderIO into a ReaderIOResult context.
// The ReaderIO computation always succeeds, so it's wrapped in the Right (success) side.
// This is an alias for RightReaderIO.
//
// Type Parameters:
// - R: The type of the environment
// - A: The type of the value produced
//
// Parameters:
// - ma: The ReaderIO to lift
//
// Returns:
// - A ReaderIOResult that executes the ReaderIO and wraps the result as a success
//
// Example:
//
// getConfigValue := func(cfg Config) func() int {
// return func() int { return cfg.Timeout }
// }
// readerIOResult := FromReaderIO(getConfigValue)
// result, err := readerIOResult(cfg)() // Returns cfg.Timeout, nil
//
//go:inline
func FromReaderIO[R, A any](ma ReaderIO[R, A]) ReaderIOResult[R, A] {
return RightReaderIO(ma)
}
// RightReaderIO lifts a ReaderIO into a ReaderIOResult as a successful value.
// The ReaderIO computation always succeeds, so it's wrapped in the Right (success) side.
//
// Type Parameters:
// - R: The type of the environment
// - A: The type of the value produced
//
// Parameters:
// - ma: The ReaderIO to lift
//
// Returns:
// - A ReaderIOResult that executes the ReaderIO and wraps the result as a success
//
// Example:
//
// logMessage := func(cfg Config) func() string {
// return func() string {
// log.Printf("Processing with timeout: %d", cfg.Timeout)
// return "logged"
// }
// }
// readerIOResult := RightReaderIO(logMessage)
func RightReaderIO[R, A any](ma ReaderIO[R, A]) ReaderIOResult[R, A] {
return function.Flow2(
ma,
ioresult.FromIO,
)
}
// MonadMap transforms the success value of a ReaderIOResult using the provided function.
// If the computation fails, the error is propagated unchanged.
//
// Type Parameters:
// - R: The type of the environment
// - A: The input type
// - B: The output type
//
// Parameters:
// - fa: The ReaderIOResult to transform
// - f: The transformation function
//
// Returns:
// - A new ReaderIOResult with the transformed value
//
// Example:
//
// getValue := Right[Config](10)
// doubled := MonadMap(getValue, func(x int) int { return x * 2 })
// result, err := doubled(cfg)() // Returns 20, nil
func MonadMap[R, A, B any](fa ReaderIOResult[R, A], f func(A) B) ReaderIOResult[R, B] {
return function.Flow2(
fa,
ioresult.Map(f),
)
}
// Map transforms the success value of a ReaderIOResult using the provided function.
// This is the curried version of MonadMap, useful for composition in pipelines.
//
// Type Parameters:
// - R: The type of the environment
// - A: The input type
// - B: The output type
//
// Parameters:
// - f: The transformation function
//
// Returns:
// - A function that transforms a ReaderIOResult
//
// Example:
//
// double := Map[Config](func(x int) int { return x * 2 })
// getValue := Right[Config](10)
// result := F.Pipe1(getValue, double)
// value, err := result(cfg)() // Returns 20, nil
func Map[R, A, B any](f func(A) B) Operator[R, A, B] {
mp := ioresult.Map(f)
return func(ri ReaderIOResult[R, A]) ReaderIOResult[R, B] {
return function.Flow2(
ri,
mp,
)
}
}
// MonadMapTo replaces the success value with a constant value.
// Useful when you want to discard the result but keep the effect.
func MonadMapTo[R, A, B any](fa ReaderIOResult[R, A], b B) ReaderIOResult[R, B] {
return MonadMap(fa, function.Constant1[A](b))
}
// MapTo returns a function that replaces the success value with a constant.
// This is the curried version of MonadMapTo.
func MapTo[R, A, B any](b B) Operator[R, A, B] {
return Map[R](function.Constant1[A](b))
}
//go:inline
func MonadChain[R, A, B any](fa ReaderIOResult[R, A], f Kleisli[R, A, B]) ReaderIOResult[R, B] {
return func(r R) IOResult[B] {
return function.Pipe1(
fa(r),
ioresult.Chain(func(a A) IOResult[B] {
return f(a)(r)
}),
)
}
}
//go:inline
func MonadChainFirst[R, A, B any](fa ReaderIOResult[R, A], f Kleisli[R, A, B]) ReaderIOResult[R, A] {
return chain.MonadChainFirst(
MonadChain[R, A, A],
MonadMap[R, B, A],
fa,
f)
}
//go:inline
func MonadTap[R, A, B any](fa ReaderIOResult[R, A], f Kleisli[R, A, B]) ReaderIOResult[R, A] {
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,
// )
// }
// // 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,
// )
// }
// // 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,
// )
// }
// //go:inline
// func MonadTapEitherK[R, A, B any](ma ReaderIOResult[R, A], f either.Kleisli[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,
// )
// }
// //go:inline
// func TapEitherK[R, A, B any](f either.Kleisli[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.
//
//go:inline
func MonadChainReaderK[R, A, B any](ma ReaderIOResult[R, A], f reader.Kleisli[R, A, B]) ReaderIOResult[R, B] {
return fromreader.MonadChainReaderK(
MonadChain[R, A, B],
FromReader[R, B],
ma,
f,
)
}
// ChainReaderK returns a function that chains a Reader-returning function into ReaderIOResult.
// This is the curried version of MonadChainReaderK.
//
//go:inline
func ChainReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, B] {
return fromreader.ChainReaderK(
Chain[R, A, B],
FromReader[R, B],
f,
)
}
//go:inline
func MonadChainFirstReaderK[R, A, B any](ma ReaderIOResult[R, A], f reader.Kleisli[R, A, B]) ReaderIOResult[R, A] {
return fromreader.MonadChainFirstReaderK(
MonadChainFirst[R, A, B],
FromReader[R, B],
ma,
f,
)
}
//go:inline
func MonadTapReaderK[R, A, B any](ma ReaderIOResult[R, A], f reader.Kleisli[R, A, B]) ReaderIOResult[R, A] {
return MonadChainFirstReaderK(ma, f)
}
// ChainReaderK returns a function that chains a Reader-returning function into ReaderIOResult.
// This is the curried version of MonadChainReaderK.
//
//go:inline
func ChainFirstReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, A] {
return fromreader.ChainFirstReaderK(
ChainFirst[R, A, B],
FromReader[R, B],
f,
)
}
//go:inline
func TapReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, A] {
return ChainFirstReaderK(f)
}
//go:inline
func MonadChainReaderIOK[R, A, B any](ma ReaderIOResult[R, A], f readerio.Kleisli[R, A, B]) ReaderIOResult[R, B] {
return fromreader.MonadChainReaderK(
MonadChain[R, A, B],
FromReaderIO[R, B],
ma,
f,
)
}
//go:inline
func ChainReaderIOK[R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, A, B] {
return fromreader.ChainReaderK(
Chain[R, A, B],
FromReaderIO[R, B],
f,
)
}
//go:inline
func MonadChainFirstReaderIOK[R, A, B any](ma ReaderIOResult[R, A], f readerio.Kleisli[R, A, B]) ReaderIOResult[R, A] {
return fromreader.MonadChainFirstReaderK(
MonadChainFirst[R, A, B],
FromReaderIO[R, B],
ma,
f,
)
}
//go:inline
func MonadTapReaderIOK[R, A, B any](ma ReaderIOResult[R, A], f readerio.Kleisli[R, A, B]) ReaderIOResult[R, A] {
return MonadChainFirstReaderIOK(ma, f)
}
//go:inline
func ChainFirstReaderIOK[R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, A, A] {
return fromreader.ChainFirstReaderK(
ChainFirst[R, A, B],
FromReaderIO[R, B],
f,
)
}
//go:inline
func TapReaderIOK[R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, A, A] {
return ChainFirstReaderIOK(f)
}
// //go:inline
// func MonadChainReaderEitherK[R, A, B any](ma ReaderIOResult[R, A], f RE.Kleisli[R, A, B]) ReaderIOResult[R, B] {
// return fromreader.MonadChainReaderK(
// MonadChain[R, A, B],
// FromReaderEither[R, B],
// ma,
// f,
// )
// }
// // ChainReaderK returns a function that chains a Reader-returning function into ReaderIOResult.
// // This is the curried version of MonadChainReaderK.
// //
// //go:inline
// func ChainReaderEitherK[R, A, B any](f RE.Kleisli[R, A, B]) Operator[R, A, B] {
// return fromreader.ChainReaderK(
// Chain[R, A, B],
// FromReaderEither[R, B],
// f,
// )
// }
// //go:inline
// func MonadChainFirstReaderEitherK[R, A, B any](ma ReaderIOResult[R, A], f RE.Kleisli[R, A, B]) ReaderIOResult[R, A] {
// return fromreader.MonadChainFirstReaderK(
// MonadChainFirst[R, A, B],
// FromReaderEither[R, B],
// ma,
// f,
// )
// }
// //go:inline
// func MonadTapReaderEitherK[R, A, B any](ma ReaderIOResult[R, A], f RE.Kleisli[R, A, B]) ReaderIOResult[R, A] {
// return MonadChainFirstReaderEitherK(ma, f)
// }
// // ChainReaderK returns a function that chains a Reader-returning function into ReaderIOResult.
// // This is the curried version of MonadChainReaderK.
// //
// //go:inline
// func ChainFirstReaderEitherK[R, A, B any](f RE.Kleisli[R, A, B]) Operator[R, A, A] {
// return fromreader.ChainFirstReaderK(
// ChainFirst[R, A, B],
// FromReaderEither[R, B],
// f,
// )
// }
// //go:inline
// func TapReaderEitherK[R, A, B any](f RE.Kleisli[R, A, B]) Operator[R, A, A] {
// return ChainFirstReaderEitherK(f)
// }
// //go:inline
// func ChainReaderOptionK[R, A, B any](onNone func() E) func(readeroption.Kleisli[R, A, B]) Operator[R, A, B] {
// fro := FromReaderOption[R, B](onNone)
// return func(f readeroption.Kleisli[R, A, B]) Operator[R, A, B] {
// return fromreader.ChainReaderK(
// Chain[R, A, B],
// fro,
// f,
// )
// }
// }
// //go:inline
// func ChainFirstReaderOptionK[R, A, B any](onNone func() E) func(readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
// fro := FromReaderOption[R, B](onNone)
// return func(f readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
// return fromreader.ChainFirstReaderK(
// ChainFirst[R, A, B],
// fro,
// f,
// )
// }
// }
// //go:inline
// func TapReaderOptionK[R, A, B any](onNone func() E) func(readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
// return ChainFirstReaderOptionK[R, A, B](onNone)
// }
// // MonadChainIOEitherK chains an IOEither-returning computation into a ReaderIOResult.
// // The IOEither is automatically lifted into the ReaderIOResult context.
// //
// //go:inline
// func MonadChainIOEitherK[R, A, B any](ma ReaderIOResult[R, A], f IOE.Kleisli[A, B]) ReaderIOResult[R, B] {
// return fromioeither.MonadChainIOEitherK(
// MonadChain[R, A, B],
// FromIOEither[R, B],
// ma,
// f,
// )
// }
// // ChainIOEitherK returns a function that chains an IOEither-returning function into ReaderIOResult.
// // This is the curried version of MonadChainIOEitherK.
// //
// //go:inline
// func ChainIOEitherK[R, A, B any](f IOE.Kleisli[A, B]) Operator[R, A, B] {
// return fromioeither.ChainIOEitherK(
// Chain[R, A, B],
// FromIOEither[R, B],
// f,
// )
// }
// MonadChainIOK chains an IO-returning computation into a ReaderIOResult.
// The IO is automatically lifted into the ReaderIOResult context (always succeeds).
//
//go:inline
func MonadChainIOK[R, A, B any](ma ReaderIOResult[R, A], f io.Kleisli[A, B]) ReaderIOResult[R, B] {
return fromio.MonadChainIOK(
MonadChain[R, A, B],
FromIO[R, B],
ma,
f,
)
}
// ChainIOK returns a function that chains an IO-returning function into ReaderIOResult.
// This is the curried version of MonadChainIOK.
//
//go:inline
func ChainIOK[R, A, B any](f io.Kleisli[A, B]) Operator[R, A, B] {
return fromio.ChainIOK(
Chain[R, A, B],
FromIO[R, B],
f,
)
}
// MonadChainFirstIOK chains an IO computation but keeps the original value.
// Useful for performing IO side effects while preserving the original value.
//
//go:inline
func MonadChainFirstIOK[R, A, B any](ma ReaderIOResult[R, A], f io.Kleisli[A, B]) ReaderIOResult[R, A] {
return fromio.MonadChainFirstIOK(
MonadChain[R, A, A],
MonadMap[R, B, A],
FromIO[R, B],
ma,
f,
)
}
//go:inline
func MonadTapIOK[R, A, B any](ma ReaderIOResult[R, A], f io.Kleisli[A, B]) ReaderIOResult[R, A] {
return MonadChainFirstIOK(ma, f)
}
// ChainFirstIOK returns a function that chains an IO computation while preserving the original value.
// This is the curried version of MonadChainFirstIOK.
//
//go:inline
func ChainFirstIOK[R, A, B any](f io.Kleisli[A, B]) Operator[R, A, A] {
return fromio.ChainFirstIOK(
Chain[R, A, A],
Map[R, B, A],
FromIO[R, B],
f,
)
}
//go:inline
func TapIOK[R, A, B any](f io.Kleisli[A, B]) Operator[R, A, A] {
return ChainFirstIOK[R](f)
}
// // ChainOptionK returns a function that chains an Option-returning function into ReaderIOResult.
// // If the Option is None, the provided error function is called to produce the error value.
// //
// //go:inline
// func ChainOptionK[R, A, B any](onNone func() E) func(func(A) Option[B]) Operator[R, A, B] {
// return fromeither.ChainOptionK(
// MonadChain[R, A, B],
// FromEither[R, B],
// onNone,
// )
// }
// MonadAp applies a function wrapped in a context to a value wrapped in a context.
// Both computations are executed (default behavior may be sequential or parallel depending on implementation).
//
//go:inline
func MonadAp[R, A, B any](fab ReaderIOResult[R, func(A) B], fa ReaderIOResult[R, A]) ReaderIOResult[R, B] {
return func(r R) IOResult[B] {
return ioresult.MonadAp(fab(r), fa(r))
}
}
// MonadApSeq applies a function in a context to a value in a context, executing them sequentially.
//
//go:inline
func MonadApSeq[R, A, B any](fab ReaderIOResult[R, func(A) B], fa ReaderIOResult[R, A]) ReaderIOResult[R, B] {
return func(r R) IOResult[B] {
return ioresult.MonadApSeq(fab(r), fa(r))
}
}
// MonadApPar applies a function in a context to a value in a context, executing them in parallel.
//
//go:inline
func MonadApPar[R, A, B any](fab ReaderIOResult[R, func(A) B], fa ReaderIOResult[R, A]) ReaderIOResult[R, B] {
return func(r R) IOResult[B] {
return ioresult.MonadApPar(fab(r), fa(r))
}
}
// Ap returns a function that applies a function in a context to a value in a context.
// This is the curried version of MonadAp.
func Ap[B, R, A any](fa ReaderIOResult[R, A]) func(fab ReaderIOResult[R, func(A) B]) ReaderIOResult[R, B] {
return function.Bind2nd(MonadAp[R, A, B], fa)
}
// Chain returns a function that sequences computations where the second depends on the first.
// This is the curried version of MonadChain.
//
//go:inline
func Chain[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, B] {
return function.Bind2nd(MonadChain, f)
}
// ChainFirst returns a function that sequences computations but keeps the first result.
// This is the curried version of MonadChainFirst.
//
//go:inline
func ChainFirst[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, A] {
return chain.ChainFirst(
Chain[R, A, A],
Map[R, B, A],
f)
}
//go:inline
func Tap[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, A] {
return ChainFirst(f)
}
// Right creates a successful ReaderIOResult with the given value.
//
//go:inline
func Right[R, A any](a A) ReaderIOResult[R, A] {
return reader.Of[R](ioresult.Right(a))
}
// Left creates a failed ReaderIOResult with the given error.
//
//go:inline
func Left[R, A any](e error) ReaderIOResult[R, A] {
return reader.Of[R](ioresult.Left[A](e))
}
// Of creates a successful ReaderIOResult with the given value.
// This is the pointed functor operation, lifting a pure value into the ReaderIOResult context.
func Of[R, A any](a A) ReaderIOResult[R, A] {
return Right[R](a)
}
// Flatten removes one level of nesting from a nested ReaderIOResult.
// Converts ReaderIOResult[R, ReaderIOResult[R, A]] to ReaderIOResult[R, A].
func Flatten[R, A any](mma ReaderIOResult[R, ReaderIOResult[R, A]]) ReaderIOResult[R, A] {
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)
// }
// 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] {
return function.Flow2(ma, ioresult.Right[A])
}
// LeftReader lifts a Reader into a ReaderIOResult, placing the result in the Left (error) side.
func LeftReader[A, R any](ma Reader[R, error]) ReaderIOResult[R, A] {
return function.Flow2(ma, ioresult.Left[A])
}
// FromReader lifts a Reader into a ReaderIOResult context.
// The Reader result is placed in the Right side (success).
func FromReader[R, A any](ma Reader[R, A]) ReaderIOResult[R, A] {
return RightReader(ma)
}
// // FromIOEither lifts an IOEither into a ReaderIOResult context.
// // The computation becomes independent of any reader context.
// //
// //go:inline
// func FromIOEither[R, A any](ma IOEither[A]) ReaderIOResult[R, A] {
// return reader.Of[R](ma)
// }
// // FromReaderEither lifts a ReaderEither into a ReaderIOResult context.
// // The Either result is lifted into an IO effect.
// func FromReaderEither[R, A any](ma RE.ReaderEither[R, A]) ReaderIOResult[R, A] {
// return function.Flow2(ma, IOE.FromEither[A])
// }
// Ask returns a ReaderIOResult that retrieves the current context.
// Useful for accessing configuration or dependencies.
//
//go:inline
func Ask[R any]() ReaderIOResult[R, R] {
return fromreader.Ask(FromReader[R, R])()
}
// Asks returns a ReaderIOResult that retrieves a value derived from the context.
// This is useful for extracting specific fields from a configuration object.
//
//go:inline
func Asks[R, A any](r Reader[R, A]) ReaderIOResult[R, A] {
return fromreader.Asks(FromReader[R, A])(r)
}
// // FromOption converts an Option to a ReaderIOResult.
// // If the Option is None, the provided function is called to produce the error.
// //
// //go:inline
// func FromOption[R, A any](onNone func() E) func(Option[A]) ReaderIOResult[R, A] {
// return fromeither.FromOption(FromEither[R, A], onNone)
// }
// // FromPredicate creates a ReaderIOResult from a predicate.
// // If the predicate returns false, the onFalse function is called to produce the error.
// //
// //go:inline
// func FromPredicate[R, A any](pred func(A) bool, onFalse func(A) E) func(A) ReaderIOResult[R, A] {
// return fromeither.FromPredicate(FromEither[R, A], pred, onFalse)
// }
// // Fold handles both success and error cases, producing a ReaderIO.
// // This is useful for converting a ReaderIOResult into a ReaderIO by handling all cases.
// //
// //go:inline
// func Fold[R, A, B any](onLeft func(E) ReaderIO[R, B], onRight func(A) ReaderIO[R, B]) func(ReaderIOResult[R, A]) ReaderIO[R, B] {
// return eithert.MatchE(readerio.MonadChain[R, either.Either[A], B], onLeft, onRight)
// }
// //go:inline
// func MonadFold[R, A, B any](ma ReaderIOResult[R, A], onLeft func(E) ReaderIO[R, B], onRight func(A) ReaderIO[R, B]) ReaderIO[R, B] {
// return eithert.FoldE(readerio.MonadChain[R, either.Either[A], B], ma, onLeft, onRight)
// }
// // GetOrElse provides a default value in case of error.
// // The default is computed lazily via a ReaderIO.
// //
// //go:inline
// func GetOrElse[R, A any](onLeft func(E) ReaderIO[R, A]) func(ReaderIOResult[R, A]) ReaderIO[R, A] {
// return eithert.GetOrElse(readerio.MonadChain[R, either.Either[A], A], readerio.Of[R, A], onLeft)
// }
// // OrElse tries an alternative computation if the first one fails.
// // The alternative can produce a different error type.
// //
// //go:inline
// func OrElse[R1, A2 any](onLeft func(E1) ReaderIOResult[R2, A]) func(ReaderIOResult[R1, A]) ReaderIOResult[R2, A] {
// return eithert.OrElse(readerio.MonadChain[R, either.Either[E1, A], either.Either[E2, A]], readerio.Of[R, either.Either[E2, A]], onLeft)
// }
// // OrLeft transforms the error using a ReaderIO if the computation fails.
// // The success value is preserved unchanged.
// //
// //go:inline
// func OrLeft[A1, R2 any](onLeft func(E1) ReaderIO[R2]) func(ReaderIOResult[R1, A]) ReaderIOResult[R2, A] {
// return eithert.OrLeft(
// readerio.MonadChain[R, either.Either[E1, A], either.Either[E2, A]],
// readerio.MonadMap[R2, either.Either[E2, A]],
// readerio.Of[R, either.Either[E2, A]],
// onLeft,
// )
// }
// // MonadBiMap applies two functions: one to the error, one to the success value.
// // This allows transforming both channels simultaneously.
// //
// //go:inline
// func MonadBiMap[R12, A, B any](fa ReaderIOResult[R1, A], f func(E1) E2, g func(A) B) ReaderIOResult[R2, B] {
// return eithert.MonadBiMap(
// readerio.MonadMap[R, either.Either[E1, A], either.Either[E2, B]],
// fa, f, g,
// )
// }
// // BiMap returns a function that maps over both the error and success channels.
// // This is the curried version of MonadBiMap.
// //
// //go:inline
// func BiMap[R12, A, B any](f func(E1) E2, g func(A) B) func(ReaderIOResult[R1, A]) ReaderIOResult[R2, B] {
// return eithert.BiMap(readerio.Map[R, either.Either[E1, A], either.Either[E2, B]], f, g)
// }
// // TryCatch wraps a function that returns (value, error) into a ReaderIOResult.
// // The onThrow function converts the error into the desired error type.
// func TryCatch[R, A any](f func(R) func() (A, error), onThrow func(error) E) ReaderIOResult[R, A] {
// return func(r R) IOEither[A] {
// return IOE.TryCatch(f(r), onThrow)
// }
// }
// // MonadAlt tries the first computation, and if it fails, tries the second.
// // This implements the Alternative pattern for error recovery.
// //
// //go:inline
// func MonadAlt[R, A any](first ReaderIOResult[R, A], second L.Lazy[ReaderIOResult[R, A]]) ReaderIOResult[R, A] {
// return eithert.MonadAlt(
// readerio.Of[Rither[A]],
// readerio.MonadChain[Rither[A]ither[A]],
// first,
// second,
// )
// }
// // Alt returns a function that tries an alternative computation if the first fails.
// // This is the curried version of MonadAlt.
// //
// //go:inline
// func Alt[R, A any](second L.Lazy[ReaderIOResult[R, A]]) Operator[R, A, A] {
// return eithert.Alt(
// readerio.Of[Rither[A]],
// readerio.Chain[Rither[A]ither[A]],
// second,
// )
// }
// // Memoize computes the value of the ReaderIOResult lazily but exactly once.
// // The context used is from the first call. Do not use if the value depends on the context.
// //
// //go:inline
// func Memoize[
// R, A any](rdr ReaderIOResult[R, A]) ReaderIOResult[R, A] {
// return readerio.Memoize(rdr)
// }
// MonadFlap applies a value to a function wrapped in a context.
// This is the reverse of Ap - the value is fixed and the function varies.
//
//go:inline
func MonadFlap[R, B, A any](fab ReaderIOResult[R, func(A) B], a A) ReaderIOResult[R, B] {
return functor.MonadFlap(MonadMap[R, func(A) B, B], fab, a)
}
// Flap returns a function that applies a fixed value to a function in a context.
// This is the curried version of MonadFlap.
//
//go:inline
func Flap[R, B, A any](a A) func(ReaderIOResult[R, func(A) B]) ReaderIOResult[R, B] {
return functor.Flap(Map[R, func(A) B, B], a)
}
// // MonadMapLeft applies a function to the error value, leaving success unchanged.
// //
// //go:inline
// func MonadMapLeft[R12, A any](fa ReaderIOResult[R1, A], f func(E1) E2) ReaderIOResult[R2, A] {
// return eithert.MonadMapLeft(readerio.MonadMap[Rither[E1, A]ither[E2, A]], fa, f)
// }
// // MapLeft returns a function that transforms the error channel.
// // This is the curried version of MonadMapLeft.
// //
// //go:inline
// func MapLeft[R, A12 any](f func(E1) E2) func(ReaderIOResult[R1, A]) ReaderIOResult[R2, A] {
// return eithert.MapLeft(readerio.Map[Rither[E1, A]ither[E2, A]], f)
// }
// Local runs a computation with a modified context.
// The function f transforms the context before passing it to the computation.
// This is similar to Contravariant's contramap operation.
//
//go:inline
func Local[A, R1, R2 any](f func(R2) R1) func(ReaderIOResult[R1, A]) ReaderIOResult[R2, A] {
return reader.Local[IOResult[A]](f)
}
//go:inline
func Read[A, R any](r R) func(ReaderIOResult[R, A]) IOResult[A] {
return reader.Read[IOResult[A]](r)
}
// //go:inline
// func MonadChainLeft[RAB, A any](fa ReaderIOResult[RA, A], f Kleisli[RBA, A]) ReaderIOResult[RB, A] {
// return readert.MonadChain(
// IOE.MonadChainLeft[EAB, A],
// fa,
// f,
// )
// }
// //go:inline
// func ChainLeft[RAB, A any](f Kleisli[RBA, A]) func(ReaderIOResult[RA, A]) ReaderIOResult[RB, A] {
// return readert.Chain[ReaderIOResult[RA, A]](
// IOE.ChainLeft[EAB, A],
// f,
// )
// }
// // MonadChainFirstLeft chains a computation on the left (error) side but always returns the original error.
// // If the input is a Left value, it applies the function f to the error and executes the resulting computation,
// // but always returns the original Left error regardless of what f returns (Left or Right).
// // If the input is a Right value, it passes through unchanged without calling f.
// //
// // This is useful for side effects on errors (like logging or metrics) where you want to perform an action
// // when an error occurs but always propagate the original error, ensuring the error path is preserved.
// //
// // Parameters:
// // - ma: The input ReaderIOResult that may contain an error of type EA
// // - f: A function that takes an error of type EA and returns a ReaderIOResult (typically for side effects)
// //
// // Returns:
// // - A ReaderIOResult with the original error preserved if input was Left, or the original Right value
// //
// //go:inline
// func MonadChainFirstLeft[A, RAB, B any](ma ReaderIOResult[RA, A], f Kleisli[RBA, B]) ReaderIOResult[RA, A] {
// return MonadChainLeft(ma, function.Flow2(f, Fold(function.Constant1[EB](ma), function.Constant1[B](ma))))
// }
// //go:inline
// func MonadTapLeft[A, RAB, B any](ma ReaderIOResult[RA, A], f Kleisli[RBA, B]) ReaderIOResult[RA, A] {
// return MonadChainFirstLeft(ma, f)
// }
// // ChainFirstLeft is the curried version of [MonadChainFirstLeft].
// // It returns a function that chains a computation on the left (error) side while always preserving the original error.
// //
// // This is particularly useful for adding error handling side effects (like logging, metrics, or notifications)
// // in a functional pipeline. The original error is always returned regardless of what f returns (Left or Right),
// // ensuring the error path is preserved.
// //
// // Parameters:
// // - f: A function that takes an error of type EA and returns a ReaderIOResult (typically for side effects)
// //
// // Returns:
// // - An Operator that performs the side effect but always returns the original error if input was Left
// //
// //go:inline
// func ChainFirstLeft[A, RAB, B any](f Kleisli[RBA, B]) Operator[RA, A, A] {
// return ChainLeft(func(e EA) ReaderIOResult[RA, A] {
// ma := Left[R, A](e)
// return MonadFold(f(e), function.Constant1[EB](ma), function.Constant1[B](ma))
// })
// }
// //go:inline
// func TapLeft[A, RAB, B any](f Kleisli[RBA, B]) Operator[RA, A, A] {
// return ChainFirstLeft[A](f)
// }

View File

@@ -0,0 +1,592 @@
// 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 (
"errors"
"fmt"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
"github.com/IBM/fp-go/v2/io"
N "github.com/IBM/fp-go/v2/number"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
type TestConfig struct {
Multiplier int
Prefix string
}
func TestFromIOResult(t *testing.T) {
t.Run("lifts successful IOResult", func(t *testing.T) {
ioResult := ioresult.Of(42)
readerIOResult := FromIOResult[TestConfig](ioResult)
cfg := TestConfig{Multiplier: 5}
result, err := readerIOResult(cfg)()
assert.NoError(t, err)
assert.Equal(t, 42, result)
})
t.Run("lifts failing IOResult", func(t *testing.T) {
expectedError := errors.New("io error")
ioResult := ioresult.Left[int](expectedError)
readerIOResult := FromIOResult[TestConfig](ioResult)
cfg := TestConfig{Multiplier: 5}
_, err := readerIOResult(cfg)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("ignores environment", func(t *testing.T) {
ioResult := ioresult.Of("constant")
readerIOResult := FromIOResult[TestConfig](ioResult)
// Different configs should produce same result
result1, _ := readerIOResult(TestConfig{Multiplier: 1})()
result2, _ := readerIOResult(TestConfig{Multiplier: 100})()
assert.Equal(t, result1, result2)
assert.Equal(t, "constant", result1)
})
}
func TestRightIO(t *testing.T) {
t.Run("lifts IO as success", func(t *testing.T) {
counter := 0
io := func() int {
counter++
return counter
}
readerIOResult := RightIO[TestConfig](io)
cfg := TestConfig{}
result, err := readerIOResult(cfg)()
assert.NoError(t, err)
assert.Equal(t, 1, result)
assert.Equal(t, 1, counter)
})
t.Run("always succeeds", func(t *testing.T) {
io := io.Of("success")
readerIOResult := RightIO[TestConfig](io)
cfg := TestConfig{}
result, err := readerIOResult(cfg)()
assert.NoError(t, err)
assert.Equal(t, "success", result)
})
}
func TestLeftIO(t *testing.T) {
t.Run("lifts IO error as failure", func(t *testing.T) {
expectedError := errors.New("io error")
io := io.Of(expectedError)
readerIOResult := LeftIO[TestConfig, int](io)
cfg := TestConfig{}
_, err := readerIOResult(cfg)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("always fails", func(t *testing.T) {
io := io.Of(errors.New("always fails"))
readerIOResult := LeftIO[TestConfig, string](io)
cfg := TestConfig{}
_, err := readerIOResult(cfg)()
assert.Error(t, err)
})
}
func TestFromReaderIO(t *testing.T) {
t.Run("lifts ReaderIO as success", func(t *testing.T) {
readerIO := func(cfg TestConfig) func() int {
return func() int {
return cfg.Multiplier * 10
}
}
readerIOResult := FromReaderIO(readerIO)
cfg := TestConfig{Multiplier: 5}
result, err := readerIOResult(cfg)()
assert.NoError(t, err)
assert.Equal(t, 50, result)
})
t.Run("uses environment", func(t *testing.T) {
readerIO := func(cfg TestConfig) func() string {
return func() string {
return fmt.Sprintf("%s:%d", cfg.Prefix, cfg.Multiplier)
}
}
readerIOResult := FromReaderIO(readerIO)
result1, _ := readerIOResult(TestConfig{Prefix: "A", Multiplier: 1})()
result2, _ := readerIOResult(TestConfig{Prefix: "B", Multiplier: 2})()
assert.Equal(t, "A:1", result1)
assert.Equal(t, "B:2", result2)
})
}
func TestMonadMap(t *testing.T) {
t.Run("transforms success value", func(t *testing.T) {
getValue := Right[TestConfig](10)
double := N.Mul(2)
result := MonadMap(getValue, double)
cfg := TestConfig{}
value, err := result(cfg)()
assert.NoError(t, err)
assert.Equal(t, 20, value)
})
t.Run("propagates error", func(t *testing.T) {
expectedError := errors.New("error")
getValue := Left[TestConfig, int](expectedError)
double := N.Mul(2)
result := MonadMap(getValue, double)
cfg := TestConfig{}
_, err := result(cfg)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("chains multiple maps", func(t *testing.T) {
getValue := Right[TestConfig](5)
result := F.Pipe3(
getValue,
Map[TestConfig](N.Mul(2)),
Map[TestConfig](N.Add(3)),
Map[TestConfig](S.Format[int]("result:%d")),
)
cfg := TestConfig{}
value, err := result(cfg)()
assert.NoError(t, err)
assert.Equal(t, "result:13", value)
})
}
func TestMap(t *testing.T) {
t.Run("curried version works in pipeline", func(t *testing.T) {
double := Map[TestConfig](N.Mul(2))
getValue := Right[TestConfig](10)
result := F.Pipe1(getValue, double)
cfg := TestConfig{}
value, err := result(cfg)()
assert.NoError(t, err)
assert.Equal(t, 20, value)
})
}
func TestMonadMapTo(t *testing.T) {
t.Run("replaces value with constant", func(t *testing.T) {
getValue := Right[TestConfig](10)
result := MonadMapTo(getValue, "constant")
cfg := TestConfig{}
value, err := result(cfg)()
assert.NoError(t, err)
assert.Equal(t, "constant", value)
})
t.Run("propagates error", func(t *testing.T) {
expectedError := errors.New("error")
getValue := Left[TestConfig, int](expectedError)
result := MonadMapTo(getValue, "constant")
cfg := TestConfig{}
_, err := result(cfg)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
}
func TestMonadChain(t *testing.T) {
t.Run("sequences dependent computations", func(t *testing.T) {
getUser := Right[TestConfig](User{ID: 1, Name: "Alice"})
getUserPosts := func(user User) ReaderIOResult[TestConfig, []string] {
return func(cfg TestConfig) IOResult[[]string] {
return func() ([]string, error) {
return []string{
fmt.Sprintf("Post 1 by %s", user.Name),
fmt.Sprintf("Post 2 by %s", user.Name),
}, nil
}
}
}
result := MonadChain(getUser, getUserPosts)
cfg := TestConfig{}
posts, err := result(cfg)()
assert.NoError(t, err)
assert.Len(t, posts, 2)
assert.Contains(t, posts[0], "Alice")
})
t.Run("propagates first error", func(t *testing.T) {
expectedError := errors.New("first error")
getUser := Left[TestConfig, User](expectedError)
getUserPosts := func(user User) ReaderIOResult[TestConfig, []string] {
return Right[TestConfig]([]string{"should not be called"})
}
result := MonadChain(getUser, getUserPosts)
cfg := TestConfig{}
_, err := result(cfg)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("propagates second error", func(t *testing.T) {
expectedError := errors.New("second error")
getUser := Right[TestConfig](User{ID: 1, Name: "Alice"})
getUserPosts := func(user User) ReaderIOResult[TestConfig, []string] {
return Left[TestConfig, []string](expectedError)
}
result := MonadChain(getUser, getUserPosts)
cfg := TestConfig{}
_, err := result(cfg)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("shares environment", func(t *testing.T) {
getValue := Ask[TestConfig]()
transform := func(cfg TestConfig) ReaderIOResult[TestConfig, string] {
return func(cfg2 TestConfig) IOResult[string] {
return func() (string, error) {
// Both should see the same config
assert.Equal(t, cfg.Multiplier, cfg2.Multiplier)
return fmt.Sprintf("%s:%d", cfg.Prefix, cfg.Multiplier), nil
}
}
}
result := MonadChain(getValue, transform)
cfg := TestConfig{Prefix: "test", Multiplier: 42}
value, err := result(cfg)()
assert.NoError(t, err)
assert.Equal(t, "test:42", value)
})
}
func TestChain(t *testing.T) {
t.Run("curried version works in pipeline", func(t *testing.T) {
double := func(x int) ReaderIOResult[TestConfig, int] {
return Right[TestConfig](x * 2)
}
result := F.Pipe1(
Right[TestConfig](10),
Chain(double),
)
cfg := TestConfig{}
value, err := result(cfg)()
assert.NoError(t, err)
assert.Equal(t, 20, value)
})
}
func TestMonadChainFirst(t *testing.T) {
t.Run("executes side effect but returns first value", func(t *testing.T) {
sideEffectCalled := false
getUser := Right[TestConfig](User{ID: 1, Name: "Alice"})
logUser := func(user User) ReaderIOResult[TestConfig, string] {
return func(cfg TestConfig) IOResult[string] {
return func() (string, error) {
sideEffectCalled = true
return "logged", nil
}
}
}
result := MonadChainFirst(getUser, logUser)
cfg := TestConfig{}
user, err := result(cfg)()
assert.NoError(t, err)
assert.Equal(t, "Alice", user.Name)
assert.True(t, sideEffectCalled)
})
t.Run("propagates first error", func(t *testing.T) {
expectedError := errors.New("first error")
getUser := Left[TestConfig, User](expectedError)
logUser := func(user User) ReaderIOResult[TestConfig, string] {
return Right[TestConfig]("should not be called")
}
result := MonadChainFirst(getUser, logUser)
cfg := TestConfig{}
_, err := result(cfg)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("propagates second error", func(t *testing.T) {
expectedError := errors.New("second error")
getUser := Right[TestConfig](User{ID: 1, Name: "Alice"})
logUser := func(user User) ReaderIOResult[TestConfig, string] {
return Left[TestConfig, string](expectedError)
}
result := MonadChainFirst(getUser, logUser)
cfg := TestConfig{}
_, err := result(cfg)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
}
func TestMonadAp(t *testing.T) {
t.Run("applies function to value", func(t *testing.T) {
fab := Right[TestConfig](N.Mul(2))
fa := Right[TestConfig](21)
result := MonadAp(fab, fa)
cfg := TestConfig{}
value, err := result(cfg)()
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("propagates function error", func(t *testing.T) {
expectedError := errors.New("function error")
fab := Left[TestConfig, func(int) int](expectedError)
fa := Right[TestConfig](21)
result := MonadAp(fab, fa)
cfg := TestConfig{}
_, err := result(cfg)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("propagates value error", func(t *testing.T) {
expectedError := errors.New("value error")
fab := Right[TestConfig](N.Mul(2))
fa := Left[TestConfig, int](expectedError)
result := MonadAp(fab, fa)
cfg := TestConfig{}
_, err := result(cfg)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
}
func TestRightAndLeft(t *testing.T) {
t.Run("Right creates successful value", func(t *testing.T) {
result := Right[TestConfig](42)
cfg := TestConfig{}
value, err := result(cfg)()
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("Left creates error", func(t *testing.T) {
expectedError := errors.New("error")
result := Left[TestConfig, int](expectedError)
cfg := TestConfig{}
_, err := result(cfg)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("Of is alias for Right", func(t *testing.T) {
result1 := Right[TestConfig](42)
result2 := Of[TestConfig](42)
cfg := TestConfig{}
value1, _ := result1(cfg)()
value2, _ := result2(cfg)()
assert.Equal(t, value1, value2)
})
}
func TestFlatten(t *testing.T) {
t.Run("removes one level of nesting", func(t *testing.T) {
inner := Right[TestConfig](42)
outer := Right[TestConfig](inner)
result := Flatten(outer)
cfg := TestConfig{}
value, err := result(cfg)()
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("propagates outer error", func(t *testing.T) {
expectedError := errors.New("outer error")
outer := Left[TestConfig, ReaderIOResult[TestConfig, int]](expectedError)
result := Flatten(outer)
cfg := TestConfig{}
_, err := result(cfg)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("propagates inner error", func(t *testing.T) {
expectedError := errors.New("inner error")
inner := Left[TestConfig, int](expectedError)
outer := Right[TestConfig](inner)
result := Flatten(outer)
cfg := TestConfig{}
_, err := result(cfg)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
}
func TestAsk(t *testing.T) {
t.Run("retrieves environment", func(t *testing.T) {
result := Ask[TestConfig]()
cfg := TestConfig{Multiplier: 42, Prefix: "test"}
value, err := result(cfg)()
assert.NoError(t, err)
assert.Equal(t, cfg, value)
})
t.Run("always succeeds", func(t *testing.T) {
result := Ask[TestConfig]()
cfg := TestConfig{}
_, err := result(cfg)()
assert.NoError(t, err)
})
}
func TestAsks(t *testing.T) {
t.Run("extracts value from environment", func(t *testing.T) {
getMultiplier := func(cfg TestConfig) int {
return cfg.Multiplier
}
result := Asks(getMultiplier)
cfg := TestConfig{Multiplier: 42}
value, err := result(cfg)()
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("works with different extractors", func(t *testing.T) {
getPrefix := func(cfg TestConfig) string {
return cfg.Prefix
}
result := Asks(getPrefix)
cfg := TestConfig{Prefix: "test"}
value, err := result(cfg)()
assert.NoError(t, err)
assert.Equal(t, "test", value)
})
}
func TestLocal(t *testing.T) {
t.Run("transforms environment", func(t *testing.T) {
// Computation that uses TestConfig
computation := func(cfg TestConfig) IOResult[string] {
return func() (string, error) {
return fmt.Sprintf("%s:%d", cfg.Prefix, cfg.Multiplier), nil
}
}
// Transform function that modifies the config
transform := func(cfg TestConfig) TestConfig {
return TestConfig{
Prefix: "modified-" + cfg.Prefix,
Multiplier: cfg.Multiplier * 2,
}
}
result := Local[string](transform)(computation)
cfg := TestConfig{Prefix: "test", Multiplier: 5}
value, err := result(cfg)()
assert.NoError(t, err)
assert.Equal(t, "modified-test:10", value)
})
}
func TestRead(t *testing.T) {
t.Run("provides environment to computation", func(t *testing.T) {
computation := func(cfg TestConfig) IOResult[int] {
return func() (int, error) {
return cfg.Multiplier * 10, nil
}
}
cfg := TestConfig{Multiplier: 5}
result := Read[int](cfg)(computation)
value, err := result()
assert.NoError(t, err)
assert.Equal(t, 50, value)
})
}
// Helper type for tests
type User struct {
ID int
Name string
}

View File

@@ -23,6 +23,7 @@ import (
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
"github.com/IBM/fp-go/v2/result"
)
@@ -53,6 +54,8 @@ type (
// It is equivalent to Reader[R, IOResult[A]] or func(R) func() (A, error).
ReaderIOResult[R, A any] = Reader[R, IOResult[A]]
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
// Monoid represents a monoid structure for ReaderIOResult values.
Monoid[R, A any] = monoid.Monoid[ReaderIOResult[R, A]]

View File

@@ -43,7 +43,7 @@ func TraverseArray[R, A, B any](f Kleisli[R, A, B]) Kleisli[R, []A, []B] {
//go:inline
func MonadTraverseArray[R, A, B any](as []A, f Kleisli[R, A, B]) ReaderResult[R, []B] {
return array.MonadTraverse[[]A](
return array.MonadTraverse(
Of[R, []B],
Map[R, []B, func(B) []B],
Ap[[]B, R, B],

View File

@@ -214,7 +214,7 @@ func BenchmarkTraverseArray(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
traversed := TraverseArray[BenchContext](kleisli)
traversed := TraverseArray(kleisli)
result := traversed(arr)
_, _ = result(ctx)
}

View File

@@ -595,7 +595,7 @@ func ApReaderS[
) Operator[R, S1, S2] {
return ApS(
setter,
FromReader[R](fa),
FromReader(fa),
)
}

View File

@@ -0,0 +1,53 @@
package readerresult
import "github.com/IBM/fp-go/v2/idiomatic/result"
// Bracket makes sure that a resource is cleaned up in the event of an error. The release action is called regardless of
// whether the body action returns and error or not.
func Bracket[
R, A, B, ANY any](
acquire Lazy[ReaderResult[R, A]],
use Kleisli[R, A, B],
release func(A, B, error) ReaderResult[R, ANY],
) ReaderResult[R, B] {
return func(r R) (B, error) {
// acquire the resource
a, aerr := acquire()(r)
if aerr != nil {
return result.Left[B](aerr)
}
b, berr := use(a)(r)
_, xerr := release(a, b, berr)(r)
if berr != nil {
return result.Left[B](berr)
}
if xerr != nil {
return result.Left[B](xerr)
}
return result.Of(b)
}
}
func WithResource[B, R, A, ANY any](
onCreate Lazy[ReaderResult[R, A]],
onRelease Kleisli[R, A, ANY],
) Kleisli[R, Kleisli[R, A, B], B] {
return func(k Kleisli[R, A, B]) ReaderResult[R, B] {
return func(r R) (B, error) {
a, aerr := onCreate()(r)
if aerr != nil {
return result.Left[B](aerr)
}
b, berr := k(a)(r)
_, xerr := onRelease(a)(r)
if berr != nil {
return result.Left[B](berr)
}
if xerr != nil {
return result.Left[B](xerr)
}
return result.Of(b)
}
}
}

View File

@@ -0,0 +1,268 @@
// 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/idiomatic/result"
"github.com/IBM/fp-go/v2/reader"
)
// Sequence swaps the order of nested environment parameters in a ReaderResult computation.
//
// This function transforms a computation that takes environment R2 and produces a ReaderResult[R1, A]
// into a Kleisli arrow that takes R1 first and returns a ReaderResult[R2, A].
//
// Type Parameters:
// - R1: The type of the inner environment (becomes the outer parameter after sequencing)
// - R2: The type of the outer environment (becomes the inner environment after sequencing)
// - A: The type of the value produced by the computation
//
// Parameters:
// - ma: A ReaderResult that depends on R2 and produces a ReaderResult[R1, A]
//
// Returns:
// - A Kleisli arrow (func(R1) ReaderResult[R2, A]) that reverses the environment order
//
// The transformation preserves error handling - if the outer computation fails, the error
// is propagated; if the inner computation fails, that error is also propagated.
//
// Example:
//
// type Database struct {
// ConnectionString string
// }
// type Config struct {
// Timeout int
// }
//
// // Original: takes Config, produces ReaderResult[Database, string]
// original := func(cfg Config) (func(Database) (string, error), error) {
// if cfg.Timeout <= 0 {
// return nil, errors.New("invalid timeout")
// }
// return func(db Database) (string, error) {
// if db.ConnectionString == "" {
// return "", errors.New("empty connection")
// }
// return fmt.Sprintf("Query on %s with timeout %d",
// db.ConnectionString, cfg.Timeout), nil
// }, nil
// }
//
// // Sequenced: takes Database first, then Config
// sequenced := Sequence(original)
// db := Database{ConnectionString: "localhost:5432"}
// cfg := Config{Timeout: 30}
// result, err := sequenced(db)(cfg)
// // result: "Query on localhost:5432 with timeout 30"
func Sequence[R1, R2, A any](ma ReaderResult[R2, ReaderResult[R1, A]]) Kleisli[R2, R1, A] {
return func(r1 R1) ReaderResult[R2, A] {
return func(r2 R2) (A, error) {
mr1, err := ma(r2)
if err != nil {
return result.Left[A](err)
}
return mr1(r1)
}
}
}
// SequenceReader swaps the order of environment parameters when the inner computation is a pure Reader.
//
// This function is similar to Sequence but specialized for cases where the inner computation
// is a Reader (pure function) rather than a ReaderResult. It transforms a ReaderResult that
// produces a Reader into a Kleisli arrow with swapped environment order.
//
// Type Parameters:
// - R1: The type of the Reader's environment (becomes the outer parameter after sequencing)
// - R2: The type of the ReaderResult's environment (becomes the inner environment after sequencing)
// - A: The type of the value produced by the computation
//
// Parameters:
// - ma: A ReaderResult[R2, Reader[R1, A]] - depends on R2 and produces a pure Reader
//
// Returns:
// - A Kleisli arrow (func(R1) ReaderResult[R2, A]) that reverses the environment order
//
// The inner Reader computation is automatically lifted into the Result context (cannot fail).
// Only the outer ReaderResult can fail with an error.
//
// Example:
//
// type Config struct {
// Multiplier int
// }
//
// // Original: takes int, produces Reader[Config, int]
// original := func(x int) (func(Config) int, error) {
// if x < 0 {
// return nil, errors.New("negative value")
// }
// return func(cfg Config) int {
// return x * cfg.Multiplier
// }, nil
// }
//
// // Sequenced: takes Config first, then int
// sequenced := SequenceReader(original)
// cfg := Config{Multiplier: 5}
// result, err := sequenced(cfg)(10)
// // result: 50, err: nil
func SequenceReader[R1, R2, A any](ma ReaderResult[R2, Reader[R1, A]]) Kleisli[R2, R1, A] {
return func(r1 R1) ReaderResult[R2, A] {
return func(r2 R2) (A, error) {
mr1, err := ma(r2)
if err != nil {
return result.Left[A](err)
}
return result.Of(mr1(r1))
}
}
}
// Traverse transforms a ReaderResult computation by applying a Kleisli arrow that introduces
// a new environment dependency, effectively swapping the environment order.
//
// This is a higher-order function that takes a Kleisli arrow and returns a function that
// can transform ReaderResult computations. It's useful for introducing environment-dependent
// transformations into existing computations while reordering the environment parameters.
//
// Type Parameters:
// - R2: The type of the original computation's environment
// - R1: The type of the new environment introduced by the Kleisli arrow
// - A: The input type to the Kleisli arrow
// - B: The output type of the transformation
//
// Parameters:
// - f: A Kleisli arrow (func(A) ReaderResult[R1, B]) that transforms A to B with R1 dependency
//
// Returns:
// - A function that transforms ReaderResult[R2, A] into a Kleisli arrow with swapped environments
//
// The transformation preserves error handling from both the original computation and the
// Kleisli arrow. The resulting computation takes R1 first, then R2.
//
// Example:
//
// type Database struct {
// Prefix string
// }
//
// // Original computation: depends on int environment
// original := func(x int) (int, error) {
// if x < 0 {
// return 0, errors.New("negative value")
// }
// return x * 2, nil
// }
//
// // Kleisli arrow: transforms int to string with Database dependency
// format := func(value int) func(Database) (string, error) {
// return func(db Database) (string, error) {
// return fmt.Sprintf("%s:%d", db.Prefix, value), nil
// }
// }
//
// // Apply Traverse
// traversed := Traverse[int](format)
// result := traversed(original)
//
// // Use with Database first, then int
// db := Database{Prefix: "ID"}
// output, err := result(db)(10)
// // output: "ID:20", err: nil
func Traverse[R2, R1, A, B any](
f Kleisli[R1, A, B],
) func(ReaderResult[R2, A]) Kleisli[R2, R1, B] {
return func(rr ReaderResult[R2, A]) Kleisli[R2, R1, B] {
return func(r1 R1) ReaderResult[R2, B] {
return func(r2 R2) (B, error) {
a, err := rr(r2)
if err != nil {
return result.Left[B](err)
}
return f(a)(r1)
}
}
}
}
// TraverseReader transforms a ReaderResult computation by applying a Reader-based Kleisli arrow,
// introducing a new environment dependency while swapping the environment order.
//
// This function is similar to Traverse but specialized for pure Reader transformations that
// cannot fail. It's useful when you want to introduce environment-dependent logic without
// adding error handling complexity.
//
// Type Parameters:
// - R2: The type of the original computation's environment
// - R1: The type of the new environment introduced by the Reader Kleisli arrow
// - A: The input type to the Kleisli arrow
// - B: The output type of the transformation
//
// Parameters:
// - f: A Reader Kleisli arrow (func(A) func(R1) B) that transforms A to B with R1 dependency
//
// Returns:
// - A function that transforms ReaderResult[R2, A] into a Kleisli arrow with swapped environments
//
// The Reader transformation is automatically lifted into the Result context. Only the original
// ReaderResult computation can fail; the Reader transformation itself is pure and cannot fail.
//
// Example:
//
// type Config struct {
// Multiplier int
// }
//
// // Original computation: depends on int environment, may fail
// original := func(x int) (int, error) {
// if x < 0 {
// return 0, errors.New("negative value")
// }
// return x * 2, nil
// }
//
// // Pure Reader transformation: multiplies by config value
// multiply := func(value int) func(Config) int {
// return func(cfg Config) int {
// return value * cfg.Multiplier
// }
// }
//
// // Apply TraverseReader
// traversed := TraverseReader[int, Config, error](multiply)
// result := traversed(original)
//
// // Use with Config first, then int
// cfg := Config{Multiplier: 5}
// output, err := result(cfg)(10)
// // output: 100 (10 * 2 * 5), err: nil
func TraverseReader[R2, R1, A, B any](
f reader.Kleisli[R1, A, B],
) func(ReaderResult[R2, A]) Kleisli[R2, R1, B] {
return func(rr ReaderResult[R2, A]) Kleisli[R2, R1, B] {
return func(r1 R1) ReaderResult[R2, B] {
return func(r2 R2) (B, error) {
a, err := rr(r2)
if err != nil {
return result.Left[B](err)
}
return result.Of(f(a)(r1))
}
}
}
}

View File

@@ -0,0 +1,699 @@
// 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 (
"errors"
"fmt"
"testing"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/reader"
"github.com/stretchr/testify/assert"
)
func TestSequence(t *testing.T) {
t.Run("sequences parameter order for simple types", func(t *testing.T) {
// Original: takes int, returns ReaderResult[string, int]
original := func(x int) (ReaderResult[string, int], error) {
if x < 0 {
return nil, errors.New("negative value")
}
return func(s string) (int, error) {
return x + len(s), nil
}, nil
}
// Sequenced: takes string first, then int
sequenced := Sequence(original)
// Test original
innerFunc1, err1 := original(10)
assert.NoError(t, err1)
result1, err2 := innerFunc1("hello")
assert.NoError(t, err2)
assert.Equal(t, 15, result1)
// Test sequenced
result2, err3 := sequenced("hello")(10)
assert.NoError(t, err3)
assert.Equal(t, 15, result2)
})
t.Run("preserves outer error", func(t *testing.T) {
expectedError := errors.New("outer error")
original := func(x int) (ReaderResult[string, int], error) {
if x < 0 {
return nil, expectedError
}
return func(s string) (int, error) {
return x + len(s), nil
}, nil
}
sequenced := Sequence(original)
// Test with error
_, err := sequenced("test")(-1)
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("preserves inner error", func(t *testing.T) {
expectedError := errors.New("inner error")
original := func(x int) (ReaderResult[string, int], error) {
return func(s string) (int, error) {
if len(s) == 0 {
return 0, expectedError
}
return x + len(s), nil
}, nil
}
sequenced := Sequence(original)
// Test with inner error
_, err := sequenced("")(10)
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("works with different types", func(t *testing.T) {
// Transform int to string
original := func(x int) (ReaderResult[string, string], error) {
return func(prefix string) (string, error) {
return fmt.Sprintf("%s-%d", prefix, x), nil
}, nil
}
sequenced := Sequence(original)
result, err := sequenced("ID")(42)
assert.NoError(t, err)
assert.Equal(t, "ID-42", result)
})
t.Run("works with struct environments", func(t *testing.T) {
type Database struct {
ConnectionString string
}
type Config struct {
Timeout int
}
original := func(cfg Config) (ReaderResult[Database, string], error) {
if cfg.Timeout <= 0 {
return nil, errors.New("invalid timeout")
}
return func(db Database) (string, error) {
if db.ConnectionString == "" {
return "", errors.New("empty connection string")
}
return fmt.Sprintf("Query on %s with timeout %d",
db.ConnectionString, cfg.Timeout), nil
}, nil
}
sequenced := Sequence(original)
db := Database{ConnectionString: "localhost:5432"}
cfg := Config{Timeout: 30}
result, err := sequenced(db)(cfg)
assert.NoError(t, err)
assert.Equal(t, "Query on localhost:5432 with timeout 30", result)
})
t.Run("works with zero values", func(t *testing.T) {
original := func(x int) (ReaderResult[string, int], error) {
return func(s string) (int, error) {
return x + len(s), nil
}, nil
}
sequenced := Sequence(original)
result, err := sequenced("")(0)
assert.NoError(t, err)
assert.Equal(t, 0, result)
})
}
func TestSequenceReader(t *testing.T) {
t.Run("sequences parameter order for Reader inner type", func(t *testing.T) {
// Original: takes int, returns Reader[string, int]
original := func(x int) (reader.Reader[string, int], error) {
if x < 0 {
return nil, errors.New("negative value")
}
return func(s string) int {
return x + len(s)
}, nil
}
// Sequenced: takes string first, then int
sequenced := SequenceReader(original)
// Test original
readerFunc, err1 := original(10)
assert.NoError(t, err1)
value1 := readerFunc("hello")
assert.Equal(t, 15, value1)
// Test sequenced
value2, err2 := sequenced("hello")(10)
assert.NoError(t, err2)
assert.Equal(t, 15, value2)
})
t.Run("preserves outer error", func(t *testing.T) {
expectedError := errors.New("outer error")
original := func(x int) (reader.Reader[string, int], error) {
if x < 0 {
return nil, expectedError
}
return func(s string) int {
return x + len(s)
}, nil
}
sequenced := SequenceReader(original)
// Test with error
_, err := sequenced("test")(-1)
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("works with different types", func(t *testing.T) {
// Transform int to string using Reader
original := func(x int) (reader.Reader[string, string], error) {
return func(prefix string) string {
return fmt.Sprintf("%s-%d", prefix, x)
}, nil
}
sequenced := SequenceReader(original)
result, err := sequenced("ID")(42)
assert.NoError(t, err)
assert.Equal(t, "ID-42", result)
})
t.Run("works with struct environments", func(t *testing.T) {
type Config struct {
Multiplier int
}
original := func(x int) (reader.Reader[Config, int], error) {
if x < 0 {
return nil, errors.New("negative value")
}
return func(cfg Config) int {
return x * cfg.Multiplier
}, nil
}
sequenced := SequenceReader(original)
cfg := Config{Multiplier: 5}
result, err := sequenced(cfg)(10)
assert.NoError(t, err)
assert.Equal(t, 50, result)
})
t.Run("works with zero values", func(t *testing.T) {
original := func(x int) (reader.Reader[string, int], error) {
return func(s string) int {
return x + len(s)
}, nil
}
sequenced := SequenceReader(original)
result, err := sequenced("")(0)
assert.NoError(t, err)
assert.Equal(t, 0, result)
})
}
func TestTraverse(t *testing.T) {
t.Run("basic transformation with environment swap", func(t *testing.T) {
// Original: ReaderResult[int, int] - takes int environment, produces int
original := func(x int) (int, error) {
if x < 0 {
return 0, errors.New("negative value")
}
return x * 2, nil
}
// Kleisli function: func(int) ReaderResult[string, int]
kleisli := func(a int) ReaderResult[string, int] {
return func(s string) (int, error) {
return a + len(s), nil
}
}
// Traverse returns: func(ReaderResult[int, int]) func(string) ReaderResult[int, int]
traversed := Traverse[int](kleisli)
result := traversed(original)
// result is func(string) ReaderResult[int, int]
// Provide string first ("hello"), then int (10)
value, err := result("hello")(10)
assert.NoError(t, err)
assert.Equal(t, 25, value) // (10 * 2) + len("hello") = 20 + 5 = 25
})
t.Run("preserves outer error", func(t *testing.T) {
expectedError := errors.New("outer error")
original := func(x int) (int, error) {
if x < 0 {
return 0, expectedError
}
return x, nil
}
kleisli := func(a int) ReaderResult[string, int] {
return func(s string) (int, error) {
return a + len(s), nil
}
}
traversed := Traverse[int](kleisli)
result := traversed(original)
// Test with negative value to trigger error
_, err := result("test")(-1)
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("preserves inner error from Kleisli", func(t *testing.T) {
expectedError := errors.New("inner error")
original := Ask[int]()
kleisli := func(a int) ReaderResult[string, int] {
return func(s string) (int, error) {
if len(s) == 0 {
return 0, expectedError
}
return a + len(s), nil
}
}
traversed := Traverse[int](kleisli)
result := traversed(original)
// Test with empty string to trigger inner error
_, err := result("")(10)
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("works with different types", func(t *testing.T) {
// Transform int to string using environment-dependent logic
original := Ask[int]()
kleisli := func(a int) ReaderResult[string, string] {
return func(prefix string) (string, error) {
return fmt.Sprintf("%s-%d", prefix, a), nil
}
}
traversed := Traverse[int](kleisli)
result := traversed(original)
value, err := result("ID")(42)
assert.NoError(t, err)
assert.Equal(t, "ID-42", value)
})
t.Run("works with struct environments", func(t *testing.T) {
type Config struct {
Multiplier int
}
type Database struct {
Prefix string
}
original := func(cfg Config) (int, error) {
if cfg.Multiplier <= 0 {
return 0, errors.New("invalid multiplier")
}
return 10 * cfg.Multiplier, nil
}
kleisli := func(value int) ReaderResult[Database, string] {
return func(db Database) (string, error) {
return fmt.Sprintf("%s:%d", db.Prefix, value), nil
}
}
traversed := Traverse[Config](kleisli)
result := traversed(original)
cfg := Config{Multiplier: 5}
db := Database{Prefix: "result"}
value, err := result(db)(cfg)
assert.NoError(t, err)
assert.Equal(t, "result:50", value)
})
t.Run("chains multiple transformations", func(t *testing.T) {
original := Ask[int]()
// First transformation: multiply by environment value
kleisli1 := func(a int) ReaderResult[int, int] {
return func(multiplier int) (int, error) {
return a * multiplier, nil
}
}
traversed := Traverse[int](kleisli1)
result := traversed(original)
value, err := result(3)(5)
assert.NoError(t, err)
assert.Equal(t, 15, value) // 5 * 3 = 15
})
t.Run("works with zero values", func(t *testing.T) {
original := Ask[int]()
kleisli := func(a int) ReaderResult[string, int] {
return func(s string) (int, error) {
return a + len(s), nil
}
}
traversed := Traverse[int](kleisli)
result := traversed(original)
value, err := result("")(0)
assert.NoError(t, err)
assert.Equal(t, 0, value)
})
t.Run("enables partial application", func(t *testing.T) {
original := Ask[int]()
kleisli := func(a int) ReaderResult[int, int] {
return func(factor int) (int, error) {
return a * factor, nil
}
}
traversed := Traverse[int](kleisli)
result := traversed(original)
// Partially apply factor
withFactor := result(3)
// Can now use with different inputs
value1, err1 := withFactor(10)
assert.NoError(t, err1)
assert.Equal(t, 30, value1)
// Reuse with different input
value2, err2 := withFactor(20)
assert.NoError(t, err2)
assert.Equal(t, 60, value2)
})
}
func TestTraverseReader(t *testing.T) {
t.Run("basic transformation with Reader dependency", func(t *testing.T) {
type Config struct {
Multiplier int
}
// Original computation
original := F.Pipe1(
Ask[int](),
Map[int](N.Mul(2)),
)
// Reader-based transformation
multiply := func(a int) func(Config) int {
return func(cfg Config) int {
return a * cfg.Multiplier
}
}
// Apply TraverseReader
traversed := TraverseReader[int](multiply)
result := traversed(original)
// Provide Config first, then int
cfg := Config{Multiplier: 5}
value, err := result(cfg)(10)
assert.NoError(t, err)
assert.Equal(t, 100, value) // (10 * 2) * 5 = 100
})
t.Run("preserves outer error", func(t *testing.T) {
type Config struct {
Multiplier int
}
expectedError := errors.New("outer error")
// Original computation that fails
original := func(x int) (int, error) {
if x < 0 {
return 0, expectedError
}
return x, nil
}
// Reader-based transformation (won't be called)
multiply := func(a int) func(Config) int {
return func(cfg Config) int {
return a * cfg.Multiplier
}
}
// Apply TraverseReader
traversed := TraverseReader[int](multiply)
result := traversed(original)
// Provide Config and negative value
cfg := Config{Multiplier: 5}
_, err := result(cfg)(-1)
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("works with different types", func(t *testing.T) {
type Database struct {
Prefix string
}
// Original computation producing an int
original := Ask[int]()
// Reader-based transformation: int -> string using Database
format := func(a int) func(Database) string {
return func(db Database) string {
return fmt.Sprintf("%s:%d", db.Prefix, a)
}
}
// Apply TraverseReader
traversed := TraverseReader[int](format)
result := traversed(original)
// Provide Database first, then int
db := Database{Prefix: "ID"}
value, err := result(db)(42)
assert.NoError(t, err)
assert.Equal(t, "ID:42", value)
})
t.Run("works with struct environments", func(t *testing.T) {
type Settings struct {
Prefix string
Suffix string
}
type Context struct {
Value int
}
// Original computation
original := func(ctx Context) (string, error) {
return fmt.Sprintf("value:%d", ctx.Value), nil
}
// Reader-based transformation using Settings
decorate := func(s string) func(Settings) string {
return func(settings Settings) string {
return settings.Prefix + s + settings.Suffix
}
}
// Apply TraverseReader
traversed := TraverseReader[Context](decorate)
result := traversed(original)
// Provide Settings first, then Context
settings := Settings{Prefix: "[", Suffix: "]"}
ctx := Context{Value: 100}
value, err := result(settings)(ctx)
assert.NoError(t, err)
assert.Equal(t, "[value:100]", value)
})
t.Run("enables partial application", func(t *testing.T) {
type Config struct {
Factor int
}
// Original computation
original := Ask[int]()
// Reader-based transformation
scale := func(a int) func(Config) int {
return func(cfg Config) int {
return a * cfg.Factor
}
}
// Apply TraverseReader
traversed := TraverseReader[int](scale)
result := traversed(original)
// Partially apply Config
cfg := Config{Factor: 3}
withConfig := result(cfg)
// Can now use with different inputs
value1, err1 := withConfig(10)
assert.NoError(t, err1)
assert.Equal(t, 30, value1)
// Reuse with different input
value2, err2 := withConfig(20)
assert.NoError(t, err2)
assert.Equal(t, 60, value2)
})
t.Run("works with zero values", func(t *testing.T) {
type Config struct {
Offset int
}
// Original computation with zero value
original := Ask[int]()
// Reader-based transformation
add := func(a int) func(Config) int {
return func(cfg Config) int {
return a + cfg.Offset
}
}
// Apply TraverseReader
traversed := TraverseReader[int](add)
result := traversed(original)
// Provide Config with zero offset and zero input
cfg := Config{Offset: 0}
value, err := result(cfg)(0)
assert.NoError(t, err)
assert.Equal(t, 0, value)
})
t.Run("chains multiple transformations", func(t *testing.T) {
type Config struct {
Multiplier int
}
// Original computation
original := func(x int) (int, error) {
return x * 2, nil
}
// Reader-based transformation
multiply := func(a int) func(Config) int {
return func(cfg Config) int {
return a * cfg.Multiplier
}
}
// Apply TraverseReader
traversed := TraverseReader[int](multiply)
result := traversed(original)
// Provide Config and execute
cfg := Config{Multiplier: 4}
value, err := result(cfg)(5)
assert.NoError(t, err)
assert.Equal(t, 40, value) // (5 * 2) * 4 = 40
})
t.Run("works with complex Reader logic", func(t *testing.T) {
type ValidationRules struct {
MinValue int
MaxValue int
}
// Original computation
original := Ask[int]()
// Reader-based transformation with validation logic
validate := func(a int) func(ValidationRules) int {
return func(rules ValidationRules) int {
if a < rules.MinValue {
return rules.MinValue
}
if a > rules.MaxValue {
return rules.MaxValue
}
return a
}
}
// Apply TraverseReader
traversed := TraverseReader[int](validate)
result := traversed(original)
// Test with value within range
rules1 := ValidationRules{MinValue: 0, MaxValue: 100}
value1, err1 := result(rules1)(50)
assert.NoError(t, err1)
assert.Equal(t, 50, value1)
// Test with value above max
rules2 := ValidationRules{MinValue: 0, MaxValue: 30}
value2, err2 := result(rules2)(50)
assert.NoError(t, err2)
assert.Equal(t, 30, value2) // Clamped to max
// Test with value below min
rules3 := ValidationRules{MinValue: 60, MaxValue: 100}
value3, err3 := result(rules3)(50)
assert.NoError(t, err3)
assert.Equal(t, 60, value3) // Clamped to min
})
}

View File

@@ -316,7 +316,7 @@ func OrElse[R, A any](onLeft Kleisli[R, error, A]) Operator[R, A, A] {
// }
// }
// result := F.Pipe1(getUserRR, readerresult.OrLeft[Config](enrichError))
func OrLeft[R, A any](onLeft reader.Kleisli[R, error, error]) Operator[R, A, A] {
func OrLeft[A, R any](onLeft reader.Kleisli[R, error, error]) Operator[R, A, A] {
return func(rr ReaderResult[R, A]) ReaderResult[R, A] {
return func(r R) (A, error) {
a, err := rr(r)

View File

@@ -248,7 +248,7 @@ func TestOrLeft(t *testing.T) {
}
}
orLeft := OrLeft[MyContext, int](enrichErr)
orLeft := OrLeft[int, MyContext](enrichErr)
v, err := F.Pipe1(Of[MyContext](42), orLeft)(defaultContext)
assert.NoError(t, err)

View File

@@ -8,15 +8,12 @@ import (
func Sequence[
HKTR2HKTR1A ~func(R2) HKTR1HKTA,
R1, R2, HKTR1HKTA, HKTA any](
mchain func(HKTR1HKTA, func(func(R1) HKTA) HKTA) HKTA,
mchain func(func(func(R1) HKTA) HKTA) func(HKTR1HKTA) HKTA,
ma HKTR2HKTR1A,
) func(R1) func(R2) HKTA {
return func(r1 R1) func(R2) HKTA {
return func(r2 R2) HKTA {
return mchain(
ma(r2),
identity.Ap[HKTA](r1),
)
return mchain(identity.Ap[HKTA](r1))(ma(r2))
}
}
}
@@ -24,15 +21,12 @@ func Sequence[
func SequenceReader[
HKTR2HKTR1A ~func(R2) HKTR1HKTA,
R1, R2, A, HKTR1HKTA, HKTA any](
mmap func(HKTR1HKTA, func(func(R1) A) A) HKTA,
mmap func(func(func(R1) A) A) func(HKTR1HKTA) HKTA,
ma HKTR2HKTR1A,
) func(R1) func(R2) HKTA {
return func(r1 R1) func(R2) HKTA {
return func(r2 R2) HKTA {
return mmap(
ma(r2),
identity.Ap[A](r1),
)
return mmap(identity.Ap[A](r1))(ma(r2))
}
}
}
@@ -41,20 +35,14 @@ func Traverse[
HKTR2A ~func(R2) HKTA,
HKTR1B ~func(R1) HKTB,
R1, R2, A, HKTR1HKTB, HKTA, HKTB any](
mmap func(HKTA, func(A) HKTR1B) HKTR1HKTB,
mchain func(HKTR1HKTB, func(func(R1) HKTB) HKTB) HKTB,
mmap func(func(A) HKTR1B) func(HKTA) HKTR1HKTB,
mchain func(func(func(R1) HKTB) HKTB) func(HKTR1HKTB) HKTB,
f func(A) HKTR1B,
) func(HKTR2A) func(R1) func(R2) HKTB {
return func(ma HKTR2A) func(R1) func(R2) HKTB {
return func(r1 R1) func(R2) HKTB {
return func(r2 R2) HKTB {
return mchain(
mmap(ma(r2), f),
identity.Ap[HKTB](r1),
)
}
}
}
return function.Flow2(
function.Bind1of2(function.Bind2of3(function.Flow3[HKTR2A, func(HKTA) HKTR1HKTB, func(HKTR1HKTB) HKTB])(mmap(f))),
function.Bind12of3(function.Flow3[func(fa R1) identity.Operator[func(R1) HKTB, HKTB], func(func(func(R1) HKTB) HKTB) func(HKTR1HKTB) HKTB, func(func(HKTR1HKTB) HKTB) func(R2) HKTB])(identity.Ap[HKTB, R1], mchain),
)
}
func TraverseReader[

View File

@@ -43,12 +43,16 @@ var (
//
// greeting := io.Of("Hello, World!")
// result := greeting() // returns "Hello, World!"
//
//go:inline
func Of[A any](a A) IO[A] {
return F.Constant(a)
}
// FromIO is an identity function that returns the IO value unchanged.
// Useful for type conversions and maintaining consistency with other monad packages.
//
//go:inline
func FromIO[A any](a IO[A]) IO[A] {
return a
}
@@ -63,6 +67,8 @@ func FromImpure[ANY ~func()](f ANY) IO[any] {
// MonadOf wraps a pure value in an IO context.
// This is an alias for Of, following the monadic naming convention.
//
//go:inline
func MonadOf[A any](a A) IO[A] {
return F.Constant(a)
}
@@ -74,7 +80,10 @@ func MonadOf[A any](a A) IO[A] {
//
// doubled := io.MonadMap(io.Of(21), N.Mul(2))
// result := doubled() // returns 42
//
//go:inline
func MonadMap[A, B any](fa IO[A], f func(A) B) IO[B] {
//go:inline
return func() B {
return f(fa())
}
@@ -87,6 +96,8 @@ func MonadMap[A, B any](fa IO[A], f func(A) B) IO[B] {
//
// double := io.Map(N.Mul(2))
// doubled := double(io.Of(21))
//
//go:inline
func Map[A, B any](f func(A) B) Operator[A, B] {
return F.Bind2nd(MonadMap[A, B], f)
}
@@ -97,29 +108,40 @@ func Map[A, B any](f func(A) B) Operator[A, B] {
// Example:
//
// always42 := io.MonadMapTo(sideEffect, 42)
//
//go:inline
func MonadMapTo[A, B any](fa IO[A], b B) IO[B] {
return MonadMap(fa, F.Constant1[A](b))
}
// MapTo returns an operator that replaces the result with a constant value.
// This is the curried version of MonadMapTo.
//
//go:inline
func MapTo[A, B any](b B) Operator[A, B] {
return Map(F.Constant1[A](b))
}
// MonadChain composes computations in sequence, using the return value of one computation to determine the next computation.
//
//go:inline
func MonadChain[A, B any](fa IO[A], f Kleisli[A, B]) IO[B] {
//go:inline
return func() B {
return f(fa())()
}
}
// Chain composes computations in sequence, using the return value of one computation to determine the next computation.
//
//go:inline
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
return F.Bind2nd(MonadChain[A, B], f)
}
// MonadApSeq implements the applicative on a single thread by first executing mab and the ma
//
//go:inline
func MonadApSeq[A, B any](mab IO[func(A) B], ma IO[A]) IO[B] {
return MonadChain(mab, F.Bind1st(MonadMap[A, B], ma))
}
@@ -139,6 +161,8 @@ func MonadApPar[A, B any](mab IO[func(A) B], ma IO[A]) IO[B] {
// MonadAp implements the `ap` operation. Depending on a feature flag this will be sequential or parallel, the preferred implementation
// is parallel
//
//go:inline
func MonadAp[A, B any](mab IO[func(A) B], ma IO[A]) IO[B] {
if useParallel {
return MonadApPar(mab, ma)
@@ -153,18 +177,24 @@ func MonadAp[A, B any](mab IO[func(A) B], ma IO[A]) IO[B] {
//
// add := func(a int) func(int) int { return func(b int) int { return a + b } }
// result := io.Ap(io.Of(2))(io.Of(add(3))) // parallel execution
//
//go:inline
func Ap[B, A any](ma IO[A]) Operator[func(A) B, B] {
return F.Bind2nd(MonadAp[A, B], ma)
}
// ApSeq returns an operator that applies a function wrapped in IO to a value wrapped in IO sequentially.
// Unlike Ap, this executes the function and value computations in sequence.
//
//go:inline
func ApSeq[B, A any](ma IO[A]) Operator[func(A) B, B] {
return Chain(F.Bind1st(MonadMap[A, B], ma))
}
// ApPar returns an operator that applies a function wrapped in IO to a value wrapped in IO in parallel.
// This explicitly uses parallel execution (same as Ap when useParallel is true).
//
//go:inline
func ApPar[B, A any](ma IO[A]) Operator[func(A) B, B] {
return F.Bind2nd(MonadApPar[A, B], ma)
}
@@ -177,6 +207,8 @@ func ApPar[B, A any](ma IO[A]) Operator[func(A) B, B] {
// nested := io.Of(io.Of(42))
// flattened := io.Flatten(nested)
// result := flattened() // returns 42
//
//go:inline
func Flatten[A any](mma IO[IO[A]]) IO[A] {
return MonadChain(mma, F.Identity)
}

View File

@@ -16,28 +16,385 @@
package ioeither
import (
"encoding/json"
"log"
"time"
"github.com/IBM/fp-go/v2/bytes"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/json"
"github.com/IBM/fp-go/v2/pair"
)
// LogJSON converts the argument to pretty printed JSON and then logs it via the format string
// Can be used with [ChainFirst]
func LogJSON[A any](prefix string) Kleisli[error, A, any] {
return func(a A) IOEither[error, any] {
// log this
return function.Pipe3(
either.TryCatchError(json.MarshalIndent(a, "", " ")),
either.Map[error](bytes.ToString),
FromEither[error, string],
Chain(func(data string) IOEither[error, any] {
return FromImpure[error](func() {
log.Printf(prefix, data)
})
}),
// Can be used with [ChainFirst] and [Tap]
func LogJSON[A any](prefix string) Kleisli[error, A, string] {
return function.Flow4(
json.MarshalIndent[A],
either.Map[error](bytes.ToString),
FromEither[error, string],
ChainIOK[error](io.Logf[string](prefix)),
)
}
// LogEntryExitF creates a customizable operator that wraps an IOEither computation with entry/exit callbacks.
//
// This is a more flexible version of LogEntryExit that allows you to provide custom callbacks for
// entry and exit events. The onEntry callback is executed before the computation starts and can
// return a "start token" (such as a timestamp, trace ID, or any context data). This token is then
// passed to the onExit callback along with the computation result, enabling correlation between
// entry and exit events.
//
// The function uses the bracket pattern to ensure that:
// - The onEntry callback is executed before the computation starts
// - The onExit callback is executed after the computation completes (success or failure)
// - The start token from onEntry is available in onExit for correlation
// - The original result is preserved and returned unchanged
// - Cleanup happens even if the computation fails
//
// Type Parameters:
// - E: The error type (Left value) of the IOEither
// - A: The success type (Right value) of the IOEither
// - STARTTOKEN: The type of the token returned by onEntry (e.g., time.Time, string, trace.Span)
// - ANY: The return type of the onExit callback (typically any or a specific type)
//
// Parameters:
// - onEntry: An IO action executed when the computation starts. Returns a STARTTOKEN that will
// be passed to onExit. Use this for logging entry, starting timers, creating trace spans, etc.
// - onExit: A Kleisli function that receives a Pair containing:
// - Head: STARTTOKEN - the token returned by onEntry
// - Tail: Either[E, A] - the result of the computation (Left for error, Right for success)
// Use this for logging exit, recording metrics, closing spans, or cleanup logic.
//
// Returns:
// - An Operator that wraps the IOEither computation with the custom entry/exit callbacks
//
// Example with timing (as used by LogEntryExit):
//
// logOp := LogEntryExitF[error, User, time.Time, any](
// func() time.Time {
// log.Printf("[entering] fetchUser")
// return time.Now() // Start token is the start time
// },
// func(res pair.Pair[time.Time, Either[error, User]]) IO[any] {
// startTime := pair.Head(res)
// result := pair.Tail(res)
// duration := time.Since(startTime).Seconds()
//
// return func() any {
// if either.IsLeft(result) {
// log.Printf("[throwing] fetchUser [%.1fs]: %v", duration, either.GetLeft(result))
// } else {
// log.Printf("[exiting] fetchUser [%.1fs]", duration)
// }
// return nil
// }
// },
// )
//
// wrapped := logOp(fetchUser(123))
//
// Example with distributed tracing:
//
// import "go.opentelemetry.io/otel/trace"
//
// tracer := otel.Tracer("my-service")
//
// traceOp := LogEntryExitF[error, Data, trace.Span, any](
// func() trace.Span {
// _, span := tracer.Start(ctx, "fetchData")
// return span // Start token is the span
// },
// func(res pair.Pair[trace.Span, Either[error, Data]]) IO[any] {
// span := pair.Head(res) // Get the span from entry
// result := pair.Tail(res)
//
// return func() any {
// if either.IsLeft(result) {
// span.RecordError(either.GetLeft(result))
// span.SetStatus(codes.Error, "operation failed")
// } else {
// span.SetStatus(codes.Ok, "operation succeeded")
// }
// span.End() // Close the span
// return nil
// }
// },
// )
//
// Example with correlation ID:
//
// type RequestContext struct {
// CorrelationID string
// StartTime time.Time
// }
//
// correlationOp := LogEntryExitF[error, Response, RequestContext, any](
// func() RequestContext {
// ctx := RequestContext{
// CorrelationID: uuid.New().String(),
// StartTime: time.Now(),
// }
// log.Printf("[%s] Request started", ctx.CorrelationID)
// return ctx
// },
// func(res pair.Pair[RequestContext, Either[error, Response]]) IO[any] {
// ctx := pair.Head(res)
// result := pair.Tail(res)
// duration := time.Since(ctx.StartTime)
//
// return func() any {
// if either.IsLeft(result) {
// log.Printf("[%s] Request failed after %v: %v",
// ctx.CorrelationID, duration, either.GetLeft(result))
// } else {
// log.Printf("[%s] Request completed after %v",
// ctx.CorrelationID, duration)
// }
// return nil
// }
// },
// )
//
// Example with metrics collection:
//
// import "github.com/prometheus/client_golang/prometheus"
//
// type MetricsToken struct {
// StartTime time.Time
// OpName string
// }
//
// metricsOp := LogEntryExitF[error, Result, MetricsToken, any](
// func() MetricsToken {
// token := MetricsToken{
// StartTime: time.Now(),
// OpName: "api_call",
// }
// requestCount.WithLabelValues(token.OpName, "started").Inc()
// return token
// },
// func(res pair.Pair[MetricsToken, Either[error, Result]]) IO[any] {
// token := pair.Head(res)
// result := pair.Tail(res)
// duration := time.Since(token.StartTime).Seconds()
//
// return func() any {
// if either.IsLeft(result) {
// requestCount.WithLabelValues(token.OpName, "error").Inc()
// requestDuration.WithLabelValues(token.OpName, "error").Observe(duration)
// } else {
// requestCount.WithLabelValues(token.OpName, "success").Inc()
// requestDuration.WithLabelValues(token.OpName, "success").Observe(duration)
// }
// return nil
// }
// },
// )
//
// Use Cases:
// - Structured logging: Integration with zap, logrus, or other structured loggers
// - Distributed tracing: OpenTelemetry, Jaeger, Zipkin integration with span management
// - Metrics collection: Recording operation durations, success/failure rates with Prometheus
// - Request correlation: Tracking requests across service boundaries with correlation IDs
// - Custom monitoring: Application-specific monitoring and alerting
// - Audit logging: Recording detailed operation information for compliance
//
// Note: LogEntryExit is implemented using LogEntryExitF with time.Time as the start token.
// Use LogEntryExitF when you need more control over the entry/exit behavior or need to
// pass custom context between entry and exit callbacks.
func LogEntryExitF[E, A, STARTTOKEN, ANY any](
onEntry IO[STARTTOKEN],
onExit io.Kleisli[pair.Pair[STARTTOKEN, Either[E, A]], ANY],
) Operator[E, A, A] {
// release: Invokes the onExit callback with the start token and computation result
// This function is called by the bracket pattern after the computation completes,
// regardless of whether it succeeded or failed. It pairs the start token (from onEntry)
// with the computation result and passes them to the onExit callback.
release := func(start pair.Pair[STARTTOKEN, IOEither[E, A]], result Either[E, A]) IO[ANY] {
return function.Pipe1(
pair.MakePair(pair.Head(start), result), // Pair the start token with the result
onExit, // Pass to the exit callback
)
}
return func(src IOEither[E, A]) IOEither[E, A] {
return io.Bracket(
// Acquire: Execute onEntry to get the start token, then pair it with the source IOEither
function.Pipe1(
onEntry, // Execute entry callback to get start token
io.Map(pair.FromTail[STARTTOKEN](src)), // Pair the token with the source computation
),
// Use: Extract and execute the IOEither computation from the pair
pair.Tail[STARTTOKEN, IOEither[E, A]],
// Release: Call onExit with the start token and result (always executed)
release,
)
}
}
// LogEntryExit creates an operator that logs the entry and exit of an IOEither computation with timing information.
//
// This function wraps an IOEither computation with automatic logging that tracks:
// - Entry: Logs when the computation starts with "[entering] <name>"
// - Exit: Logs when the computation completes successfully with "[exiting ] <name> [duration]"
// - Error: Logs when the computation fails with "[throwing] <name> [duration]: <error>"
//
// The duration is measured in seconds with one decimal place precision (e.g., "2.5s").
// This is particularly useful for debugging, performance monitoring, and understanding the
// execution flow of complex IOEither chains.
//
// Type Parameters:
// - E: The error type (Left value) of the IOEither
// - A: The success type (Right value) of the IOEither
//
// Parameters:
// - name: A descriptive name for the computation, used in log messages to identify the operation
//
// Returns:
// - An Operator that wraps the IOEither computation with entry/exit logging
//
// The function uses the bracket pattern to ensure that:
// - Entry is logged before the computation starts
// - Exit/error is logged after the computation completes, regardless of success or failure
// - Timing is accurate, measuring from entry to exit
// - The original result is preserved and returned unchanged
//
// Log Format:
// - Entry: "[entering] <name>"
// - Success: "[exiting ] <name> [<duration>s]"
// - Error: "[throwing] <name> [<duration>s]: <error>"
//
// Example with successful computation:
//
// fetchUser := func(id int) IOEither[error, User] {
// return TryCatch(func() (User, error) {
// // Simulate database query
// time.Sleep(100 * time.Millisecond)
// return User{ID: id, Name: "Alice"}, nil
// })
// }
//
// // Wrap with logging
// loggedFetch := LogEntryExit[error, User]("fetchUser")(fetchUser(123))
//
// // Execute
// result := loggedFetch()
// // Logs:
// // [entering] fetchUser
// // [exiting ] fetchUser [0.1s]
//
// Example with error:
//
// failingOp := func() IOEither[error, string] {
// return TryCatch(func() (string, error) {
// time.Sleep(50 * time.Millisecond)
// return "", errors.New("connection timeout")
// })
// }
//
// logged := LogEntryExit[error, string]("failingOp")(failingOp())
// result := logged()
// // Logs:
// // [entering] failingOp
// // [throwing] failingOp [0.1s]: connection timeout
//
// Example with chained operations:
//
// pipeline := F.Pipe3(
// fetchUser(123),
// LogEntryExit[error, User]("fetchUser"),
// Chain(func(user User) IOEither[error, []Order] {
// return fetchOrders(user.ID)
// }),
// LogEntryExit[error, []Order]("fetchOrders"),
// )
// // Logs each step with timing:
// // [entering] fetchUser
// // [exiting ] fetchUser [0.1s]
// // [entering] fetchOrders
// // [exiting ] fetchOrders [0.2s]
//
// Example for performance monitoring:
//
// slowQuery := func() IOEither[error, []Record] {
// return TryCatch(func() ([]Record, error) {
// // Simulate slow database query
// time.Sleep(2 * time.Second)
// return []Record{{ID: 1}}, nil
// })
// }
//
// monitored := LogEntryExit[error, []Record]("slowQuery")(slowQuery())
// result := monitored()
// // Logs:
// // [entering] slowQuery
// // [exiting ] slowQuery [2.0s]
// // Helps identify performance bottlenecks
//
// Example with custom error types:
//
// type AppError struct {
// Code int
// Message string
// }
//
// func (e AppError) Error() string {
// return fmt.Sprintf("Error %d: %s", e.Code, e.Message)
// }
//
// operation := func() IOEither[AppError, Data] {
// return Left[Data](AppError{Code: 404, Message: "Not Found"})
// }
//
// logged := LogEntryExit[AppError, Data]("operation")(operation())
// result := logged()
// // Logs:
// // [entering] operation
// // [throwing] operation [0.0s]: Error 404: Not Found
//
// Use Cases:
// - Debugging: Track execution flow through complex IOEither chains
// - Performance monitoring: Identify slow operations with timing information
// - Production logging: Monitor critical operations in production systems
// - Testing: Verify that operations are executed in the expected order
// - Troubleshooting: Quickly identify where errors occur in a pipeline
//
// Note: This function uses Go's standard log package. For production systems,
// consider using a structured logging library and adapting this pattern to
// support different log levels and structured fields.
func LogEntryExit[E, A any](name string) Operator[E, A, A] {
return LogEntryExitF(
func() time.Time {
log.Printf("[entering] %s", name)
return time.Now()
},
func(res pair.Pair[time.Time, Either[E, A]]) IO[any] {
duration := time.Since(pair.Head(res)).Seconds()
return func() any {
onError := func(err E) any {
log.Printf("[throwing] %s [%.1fs]: %v", name, duration, err)
return nil
}
onSuccess := func(_ A) any {
log.Printf("[exiting ] %s [%.1fs]", name, duration)
return nil
}
return function.Pipe2(
res,
pair.Tail,
either.Fold(onError, onSuccess),
)
}
},
)
}

View File

@@ -16,10 +16,13 @@
package ioeither
import (
"fmt"
"testing"
"time"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/stretchr/testify/assert"
)
@@ -40,3 +43,42 @@ func TestLogging(t *testing.T) {
dst := res()
assert.Equal(t, E.Of[error](src), dst)
}
func TestLogEntryExit(t *testing.T) {
t.Run("fast and successful", func(t *testing.T) {
data := F.Pipe2(
Of[error]("test"),
ChainIOK[error](io.Logf[string]("Data: %s")),
LogEntryExit[error, string]("fast"),
)
assert.Equal(t, E.Of[error]("test"), data())
})
t.Run("slow and successful", func(t *testing.T) {
data := F.Pipe3(
Of[error]("test"),
Delay[error, string](1*time.Second),
ChainIOK[error](io.Logf[string]("Data: %s")),
LogEntryExit[error, string]("slow"),
)
assert.Equal(t, E.Of[error]("test"), data())
})
t.Run("with error", func(t *testing.T) {
err := fmt.Errorf("failure")
data := F.Pipe2(
Left[string](err),
ChainIOK[error](io.Logf[string]("Data: %s")),
LogEntryExit[error, string]("error"),
)
assert.Equal(t, E.Left[string](err), data())
})
}

View File

@@ -16,13 +16,28 @@
package ioresult
import (
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/pair"
)
// LogJSON converts the argument to pretty printed JSON and then logs it via the format string
// Can be used with [ChainFirst]
//
//go:inline
func LogJSON[A any](prefix string) Kleisli[A, any] {
func LogJSON[A any](prefix string) Kleisli[A, string] {
return ioeither.LogJSON[A](prefix)
}
//go:inline
func LogEntryExitF[A, STARTTOKEN, ANY any](
onEntry IO[STARTTOKEN],
onExit io.Kleisli[pair.Pair[STARTTOKEN, Result[A]], ANY],
) Operator[A, A] {
return ioeither.LogEntryExitF(onEntry, onExit)
}
//go:inline
func LogEntryExit[A any](name string) Operator[A, A] {
return ioeither.LogEntryExit[error, A](name)
}

View File

@@ -78,3 +78,112 @@ func Unmarshal[A any](data []byte) Either[A] {
func Marshal[A any](a A) Either[[]byte] {
return E.TryCatchError(json.Marshal(a))
}
// MarshalIndent converts a Go value to pretty-printed JSON-encoded bytes with indentation.
//
// This function wraps the standard json.MarshalIndent in an Either monad, converting the traditional
// (value, error) tuple into a functional Either type. If marshaling succeeds, it returns Right[[]byte]
// containing the formatted JSON. If it fails, it returns Left[error].
//
// The function uses a default indentation of two spaces (" ") with no prefix, making the output
// human-readable and suitable for display, logging, or configuration files. Each JSON element begins
// on a new line, and nested structures are indented to show their hierarchy.
//
// Type parameter A specifies the type of value to marshal. The type must be compatible with
// JSON encoding rules (same as json.Marshal).
//
// The function uses the same encoding rules as the standard library's json.MarshalIndent, including:
// - Support for struct tags to control field names and omitempty behavior
// - Custom MarshalJSON methods for types that implement json.Marshaler
// - Standard type conversions (strings, numbers, booleans, arrays, slices, maps, structs)
// - Proper escaping of special characters in strings
//
// Example with a simple struct:
//
// type Person struct {
// Name string `json:"name"`
// Age int `json:"age"`
// }
//
// person := Person{Name: "Alice", Age: 30}
// result := json.MarshalIndent(person)
// // result is Either[error, []byte]
//
// either.Map(func(data []byte) string {
// return string(data)
// })(result)
// // Returns Either[error, string] with formatted JSON:
// // {
// // "name": "Alice",
// // "age": 30
// // }
//
// Example with nested structures:
//
// type Address struct {
// Street string `json:"street"`
// City string `json:"city"`
// }
//
// type Employee struct {
// Name string `json:"name"`
// Address Address `json:"address"`
// }
//
// emp := Employee{
// Name: "Bob",
// Address: Address{Street: "123 Main St", City: "Boston"},
// }
// result := json.MarshalIndent(emp)
// // Produces formatted JSON:
// // {
// // "name": "Bob",
// // "address": {
// // "street": "123 Main St",
// // "city": "Boston"
// // }
// // }
//
// Example with error handling:
//
// type Config struct {
// Settings map[string]interface{} `json:"settings"`
// }
//
// config := Config{Settings: map[string]interface{}{"debug": true}}
// result := json.MarshalIndent(config)
//
// either.Fold(
// func(err error) string {
// return fmt.Sprintf("Failed to marshal: %v", err)
// },
// func(data []byte) string {
// return string(data)
// },
// )(result)
//
// Example with functional composition:
//
// // Chain operations using Either monad
// result := F.Pipe2(
// person,
// json.MarshalIndent[Person],
// either.Map(func(data []byte) string {
// return string(data)
// }),
// )
// // result is Either[error, string] with formatted JSON
//
// Use MarshalIndent when you need human-readable JSON output for:
// - Configuration files that humans will read or edit
// - Debug output and logging
// - API responses for development/testing
// - Documentation examples
//
// Use Marshal (without indentation) when:
// - Minimizing payload size is important (production APIs)
// - The JSON will be consumed by machines only
// - Performance is critical (indentation adds overhead)
func MarshalIndent[A any](a A) Either[[]byte] {
return E.TryCatchError(json.MarshalIndent(a, "", " "))
}

View File

@@ -68,8 +68,8 @@ func Traverse[R2, R1, A, B any](
f Kleisli[R1, A, B],
) func(Reader[R2, A]) Kleisli[R2, R1, B] {
return readert.Traverse[Reader[R2, A]](
identity.MonadMap,
identity.MonadChain,
identity.Map,
identity.Chain,
f,
)
}

View File

@@ -78,7 +78,7 @@ import (
// // result is Either[error, string]
func Sequence[R1, R2, E, A any](ma ReaderEither[R2, E, ReaderEither[R1, E, A]]) Kleisli[R2, E, R1, A] {
return readert.Sequence(
either.MonadChain,
either.Chain,
ma,
)
}
@@ -137,7 +137,7 @@ func Sequence[R1, R2, E, A any](ma ReaderEither[R2, E, ReaderEither[R1, E, A]])
// // result is Either[error, string]
func SequenceReader[R1, R2, E, A any](ma ReaderEither[R2, E, Reader[R1, A]]) Kleisli[R2, E, R1, A] {
return readert.SequenceReader(
either.MonadMap,
either.Map,
ma,
)
}
@@ -209,8 +209,8 @@ func Traverse[R2, R1, E, A, B any](
f Kleisli[R1, E, A, B],
) func(ReaderEither[R2, E, A]) Kleisli[R2, E, R1, B] {
return readert.Traverse[ReaderEither[R2, E, A]](
either.MonadMap,
either.MonadChain,
either.Map,
either.Chain,
f,
)
}

32
v2/readerio/bracket.go Normal file
View File

@@ -0,0 +1,32 @@
package readerio
import (
G "github.com/IBM/fp-go/v2/internal/bracket"
)
//go:inline
func Bracket[
R, A, B, ANY any](
acquire ReaderIO[R, A],
use Kleisli[R, A, B],
release func(A, B) ReaderIO[R, ANY],
) ReaderIO[R, B] {
return G.Bracket[
ReaderIO[R, A],
ReaderIO[R, B],
ReaderIO[R, ANY],
B,
A,
B,
](
Of[R, B],
MonadChain[R, A, B],
MonadChain[R, B, B],
MonadChain[R, ANY, B],
acquire,
use,
release,
)
}

View File

@@ -73,7 +73,7 @@ import (
// // result is IO[string]
func Sequence[R1, R2, A any](ma ReaderIO[R2, ReaderIO[R1, A]]) Kleisli[R2, R1, A] {
return readert.Sequence(
io.MonadChain,
io.Chain,
ma,
)
}
@@ -139,7 +139,7 @@ func Sequence[R1, R2, A any](ma ReaderIO[R2, ReaderIO[R1, A]]) Kleisli[R2, R1, A
// - Optimizing by controlling which environment access triggers IO effects
func SequenceReader[R1, R2, A any](ma ReaderIO[R2, Reader[R1, A]]) Kleisli[R2, R1, A] {
return readert.SequenceReader(
io.MonadMap,
io.Map,
ma,
)
}
@@ -148,8 +148,8 @@ func Traverse[R2, R1, A, B any](
f Kleisli[R1, A, B],
) func(ReaderIO[R2, A]) Kleisli[R2, R1, B] {
return readert.Traverse[ReaderIO[R2, A]](
io.MonadMap,
io.MonadChain,
io.Map,
io.Chain,
f,
)
}

View File

@@ -44,7 +44,7 @@ import (
// The function preserves error handling and IO effects at both levels.
func Sequence[R1, R2, E, A any](ma ReaderIOEither[R2, E, ReaderIOEither[R1, E, A]]) reader.Kleisli[R2, R1, IOEither[E, A]] {
return readert.Sequence(
ioeither.MonadChain,
ioeither.Chain,
ma,
)
}
@@ -67,7 +67,7 @@ func Sequence[R1, R2, E, A any](ma ReaderIOEither[R2, E, ReaderIOEither[R1, E, A
// - A reader.Kleisli[R2, R1, IOEither[E, A]], which is func(R2) func(R1) IOEither[E, A]
func SequenceReader[R1, R2, E, A any](ma ReaderIOEither[R2, E, Reader[R1, A]]) reader.Kleisli[R2, R1, IOEither[E, A]] {
return readert.SequenceReader(
ioeither.MonadMap,
ioeither.Map,
ma,
)
}
@@ -159,12 +159,64 @@ func Traverse[R2, R1, E, A, B any](
f Kleisli[R1, E, A, B],
) func(ReaderIOEither[R2, E, A]) Kleisli[R2, E, R1, B] {
return readert.Traverse[ReaderIOEither[R2, E, A]](
ioeither.MonadMap,
ioeither.MonadChain,
ioeither.Map,
ioeither.Chain,
f,
)
}
// TraverseReader transforms a ReaderIOEither computation by applying a Reader-based function,
// effectively introducing a new environment dependency.
//
// This function takes a Reader-based transformation (Kleisli arrow) and returns a function that
// can transform a ReaderIOEither. The result allows you to provide the Reader's environment (R1)
// first, which then produces a ReaderIOEither that depends on environment R2.
//
// Type Parameters:
// - R2: The outer environment type (from the original ReaderIOEither)
// - R1: The inner environment type (introduced by the Reader transformation)
// - E: The error type
// - A: The input value type
// - B: The output value type
//
// Parameters:
// - f: A Reader-based Kleisli arrow that transforms A to B using environment R1
//
// Returns:
// - A function that takes a ReaderIOEither[R2, E, A] and returns a Kleisli[R2, E, R1, B],
// which is func(R2) ReaderIOEither[R1, E, B]
//
// The function preserves error handling and IO effects while adding the Reader environment dependency.
//
// Example:
//
// type Config struct {
// Multiplier int
// }
// type Database struct {
// ConnectionString string
// }
//
// // Original computation that depends on Database
// original := func(db Database) IOEither[error, int] {
// return ioeither.Right[error](len(db.ConnectionString))
// }
//
// // Reader-based transformation that depends on Config
// multiply := func(x int) func(Config) int {
// return func(cfg Config) int {
// return x * cfg.Multiplier
// }
// }
//
// // Apply TraverseReader to introduce Config dependency
// traversed := TraverseReader[Database, Config, error, int, int](multiply)
// result := traversed(original)
//
// // Provide Config first, then Database
// cfg := Config{Multiplier: 5}
// db := Database{ConnectionString: "localhost:5432"}
// finalResult := result(cfg)(db)() // Returns Right(80) = len("localhost:5432") * 5
func TraverseReader[R2, R1, E, A, B any](
f reader.Kleisli[R1, A, B],
) func(ReaderIOEither[R2, E, A]) Kleisli[R2, E, R1, B] {

View File

@@ -21,7 +21,9 @@ import (
"testing"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/ioeither"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
"github.com/stretchr/testify/assert"
@@ -555,3 +557,317 @@ func TestTraverse(t *testing.T) {
assert.Equal(t, 0, value)
})
}
func TestTraverseReader(t *testing.T) {
t.Run("basic transformation with Reader dependency", func(t *testing.T) {
type Config struct {
Multiplier int
}
// Original computation
original := F.Pipe1(
Ask[int, error](),
Map[int, error](N.Mul(2)),
)
// Reader-based transformation
multiply := func(a int) func(Config) int {
return func(cfg Config) int {
return a * cfg.Multiplier
}
}
// Apply TraverseReader
traversed := TraverseReader[int, Config, error](multiply)
result := traversed(original)
// Provide Config first, then int
cfg := Config{Multiplier: 5}
innerFunc := result(cfg)
finalResult := innerFunc(10)()
value, err := either.Unwrap(finalResult)
assert.NoError(t, err)
assert.Equal(t, 100, value) // (10 * 2) * 5 = 100
})
t.Run("preserves outer error", func(t *testing.T) {
type Config struct {
Multiplier int
}
expectedError := errors.New("outer error")
// Original computation that fails
original := func(x int) IOEither[error, int] {
if x < 0 {
return ioeither.Left[int](expectedError)
}
return ioeither.Right[error](x)
}
// Reader-based transformation (won't be called)
multiply := func(a int) func(Config) int {
return func(cfg Config) int {
return a * cfg.Multiplier
}
}
// Apply TraverseReader
traversed := TraverseReader[int, Config, error](multiply)
result := traversed(original)
// Provide Config and negative value
cfg := Config{Multiplier: 5}
innerFunc := result(cfg)
finalResult := innerFunc(-1)()
_, err := either.Unwrap(finalResult)
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("works with different types", func(t *testing.T) {
type Database struct {
Prefix string
}
// Original computation producing an int
original := Ask[int, error]()
// Reader-based transformation: int -> string using Database
format := func(a int) func(Database) string {
return func(db Database) string {
return fmt.Sprintf("%s:%d", db.Prefix, a)
}
}
// Apply TraverseReader
traversed := TraverseReader[int, Database, error](format)
result := traversed(original)
// Provide Database first, then int
db := Database{Prefix: "ID"}
innerFunc := result(db)
finalResult := innerFunc(42)()
value, err := either.Unwrap(finalResult)
assert.NoError(t, err)
assert.Equal(t, "ID:42", value)
})
t.Run("works with struct environments", func(t *testing.T) {
type Settings struct {
Prefix string
Suffix string
}
type Context struct {
Value int
}
// Original computation
original := func(ctx Context) IOEither[error, string] {
return ioeither.Right[error](fmt.Sprintf("value:%d", ctx.Value))
}
// Reader-based transformation using Settings
decorate := func(s string) func(Settings) string {
return func(settings Settings) string {
return settings.Prefix + s + settings.Suffix
}
}
// Apply TraverseReader
traversed := TraverseReader[Context, Settings, error](decorate)
result := traversed(original)
// Provide Settings first, then Context
settings := Settings{Prefix: "[", Suffix: "]"}
ctx := Context{Value: 100}
innerFunc := result(settings)
finalResult := innerFunc(ctx)()
value, err := either.Unwrap(finalResult)
assert.NoError(t, err)
assert.Equal(t, "[value:100]", value)
})
t.Run("enables partial application", func(t *testing.T) {
type Config struct {
Factor int
}
// Original computation
original := Ask[int, error]()
// Reader-based transformation
scale := func(a int) func(Config) int {
return func(cfg Config) int {
return a * cfg.Factor
}
}
// Apply TraverseReader
traversed := TraverseReader[int, Config, error](scale)
result := traversed(original)
// Partially apply Config
cfg := Config{Factor: 3}
withConfig := result(cfg)
// Can now use with different inputs
finalResult1 := withConfig(10)()
value1, err1 := either.Unwrap(finalResult1)
assert.NoError(t, err1)
assert.Equal(t, 30, value1)
// Reuse with different input
finalResult2 := withConfig(20)()
value2, err2 := either.Unwrap(finalResult2)
assert.NoError(t, err2)
assert.Equal(t, 60, value2)
})
t.Run("preserves IO effects", func(t *testing.T) {
type Config struct {
Value int
}
outerCounter := 0
innerCounter := 0
// Original computation with IO effects
original := func(x int) IOEither[error, int] {
return func() either.Either[error, int] {
outerCounter++
return either.Right[error](x)
}
}
// Reader-based transformation
multiply := func(a int) func(Config) int {
return func(cfg Config) int {
innerCounter++
return a * cfg.Value
}
}
// Apply TraverseReader
traversed := TraverseReader[int, Config, error](multiply)
result := traversed(original)
// Execute multiple times to verify IO effects
cfg := Config{Value: 5}
innerFunc := result(cfg)
innerFunc(10)()
innerFunc(10)()
assert.Equal(t, 2, outerCounter)
assert.Equal(t, 2, innerCounter)
})
t.Run("works with zero values", func(t *testing.T) {
type Config struct {
Offset int
}
// Original computation with zero value
original := Ask[int, error]()
// Reader-based transformation
add := func(a int) func(Config) int {
return func(cfg Config) int {
return a + cfg.Offset
}
}
// Apply TraverseReader
traversed := TraverseReader[int, Config, error](add)
result := traversed(original)
// Provide Config with zero offset and zero input
cfg := Config{Offset: 0}
innerFunc := result(cfg)
finalResult := innerFunc(0)()
value, err := either.Unwrap(finalResult)
assert.NoError(t, err)
assert.Equal(t, 0, value)
})
t.Run("chains multiple transformations", func(t *testing.T) {
type Config struct {
Multiplier int
}
// Original computation
original := func(x int) IOEither[error, int] {
return ioeither.Right[error](x * 2)
}
// Reader-based transformation
multiply := func(a int) func(Config) int {
return func(cfg Config) int {
return a * cfg.Multiplier
}
}
// Apply TraverseReader
traversed := TraverseReader[int, Config, error](multiply)
result := traversed(original)
// Provide Config and execute
cfg := Config{Multiplier: 4}
innerFunc := result(cfg)
finalResult := innerFunc(5)()
value, err := either.Unwrap(finalResult)
assert.NoError(t, err)
assert.Equal(t, 40, value) // (5 * 2) * 4 = 40
})
t.Run("works with complex Reader logic", func(t *testing.T) {
type ValidationRules struct {
MinValue int
MaxValue int
}
// Original computation
original := Ask[int, error]()
// Reader-based transformation with validation logic
validate := func(a int) func(ValidationRules) int {
return func(rules ValidationRules) int {
if a < rules.MinValue {
return rules.MinValue
}
if a > rules.MaxValue {
return rules.MaxValue
}
return a
}
}
// Apply TraverseReader
traversed := TraverseReader[int, ValidationRules, error](validate)
result := traversed(original)
// Test with value within range
rules1 := ValidationRules{MinValue: 0, MaxValue: 100}
innerFunc1 := result(rules1)
finalResult1 := innerFunc1(50)()
value1, err1 := either.Unwrap(finalResult1)
assert.NoError(t, err1)
assert.Equal(t, 50, value1)
// Test with value above max
rules2 := ValidationRules{MinValue: 0, MaxValue: 30}
innerFunc2 := result(rules2)
finalResult2 := innerFunc2(50)()
value2, err2 := either.Unwrap(finalResult2)
assert.NoError(t, err2)
assert.Equal(t, 30, value2) // Clamped to max
// Test with value below min
rules3 := ValidationRules{MinValue: 60, MaxValue: 100}
innerFunc3 := result(rules3)
finalResult3 := innerFunc3(50)()
value3, err3 := either.Unwrap(finalResult3)
assert.NoError(t, err3)
assert.Equal(t, 60, value3) // Clamped to min
})
}

View File

@@ -77,7 +77,7 @@ import (
// // result is Option[string]
func Sequence[R1, R2, A any](ma ReaderOption[R2, ReaderOption[R1, A]]) reader.Kleisli[R2, R1, Option[A]] {
return readert.Sequence(
option.MonadChain,
option.Chain,
ma,
)
}
@@ -135,7 +135,7 @@ func Sequence[R1, R2, A any](ma ReaderOption[R2, ReaderOption[R1, A]]) reader.Kl
// // result is Option[string]
func SequenceReader[R1, R2, A any](ma ReaderOption[R2, Reader[R1, A]]) reader.Kleisli[R2, R1, Option[A]] {
return readert.SequenceReader(
option.MonadMap,
option.Map,
ma,
)
}
@@ -144,8 +144,8 @@ func Traverse[R2, R1, A, B any](
f Kleisli[R1, A, B],
) func(ReaderOption[R2, A]) Kleisli[R2, R1, B] {
return readert.Traverse[ReaderOption[R2, A]](
option.MonadMap,
option.MonadChain,
option.Map,
option.Chain,
f,
)
}