mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-09 23:11:40 +02:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba10d8d314 | ||
|
|
3d6c419185 | ||
|
|
3f4b6292e4 |
@@ -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()
|
||||
}
|
||||
|
||||
16
v2/context/readerio/bracket.go
Normal file
16
v2/context/readerio/bracket.go
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
379
v2/context/readerioresult/logging.go
Normal file
379
v2/context/readerioresult/logging.go
Normal 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),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
39
v2/context/readerioresult/logging_test.go
Normal file
39
v2/context/readerioresult/logging_test.go
Normal 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())())
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
75
v2/idiomatic/context/readerresult/array.go
Normal file
75
v2/idiomatic/context/readerresult/array.go
Normal 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)
|
||||
}
|
||||
337
v2/idiomatic/context/readerresult/bind.go
Normal file
337
v2/idiomatic/context/readerresult/bind.go
Normal 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)
|
||||
}
|
||||
403
v2/idiomatic/context/readerresult/bracket.go
Normal file
403
v2/idiomatic/context/readerresult/bracket.go
Normal 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])
|
||||
}
|
||||
210
v2/idiomatic/context/readerresult/curry.go
Normal file
210
v2/idiomatic/context/readerresult/curry.go
Normal 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)
|
||||
}
|
||||
178
v2/idiomatic/context/readerresult/doc.go
Normal file
178
v2/idiomatic/context/readerresult/doc.go
Normal 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
|
||||
107
v2/idiomatic/context/readerresult/flip.go
Normal file
107
v2/idiomatic/context/readerresult/flip.go
Normal 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)
|
||||
}
|
||||
134
v2/idiomatic/context/readerresult/from.go
Normal file
134
v2/idiomatic/context/readerresult/from.go
Normal 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)
|
||||
}
|
||||
120
v2/idiomatic/context/readerresult/monoid.go
Normal file
120
v2/idiomatic/context/readerresult/monoid.go
Normal 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)
|
||||
}
|
||||
822
v2/idiomatic/context/readerresult/reader.go
Normal file
822
v2/idiomatic/context/readerresult/reader.go
Normal 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)
|
||||
})
|
||||
}
|
||||
952
v2/idiomatic/context/readerresult/reader_test.go
Normal file
952
v2/idiomatic/context/readerresult/reader_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
133
v2/idiomatic/context/readerresult/sequence.go
Normal file
133
v2/idiomatic/context/readerresult/sequence.go
Normal file
@@ -0,0 +1,133 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package 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)
|
||||
}
|
||||
57
v2/idiomatic/context/readerresult/types.go
Normal file
57
v2/idiomatic/context/readerresult/types.go
Normal 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]
|
||||
)
|
||||
259
v2/idiomatic/readerioresult/flip.go
Normal file
259
v2/idiomatic/readerioresult/flip.go
Normal 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,
|
||||
)
|
||||
}
|
||||
865
v2/idiomatic/readerioresult/flip_test.go
Normal file
865
v2/idiomatic/readerioresult/flip_test.go
Normal 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
|
||||
})
|
||||
}
|
||||
987
v2/idiomatic/readerioresult/reader.go
Normal file
987
v2/idiomatic/readerioresult/reader.go
Normal 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)
|
||||
// }
|
||||
592
v2/idiomatic/readerioresult/reader_test.go
Normal file
592
v2/idiomatic/readerioresult/reader_test.go
Normal 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
|
||||
}
|
||||
@@ -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]]
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -595,7 +595,7 @@ func ApReaderS[
|
||||
) Operator[R, S1, S2] {
|
||||
return ApS(
|
||||
setter,
|
||||
FromReader[R](fa),
|
||||
FromReader(fa),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
53
v2/idiomatic/readerresult/bracket.go
Normal file
53
v2/idiomatic/readerresult/bracket.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
268
v2/idiomatic/readerresult/flip.go
Normal file
268
v2/idiomatic/readerresult/flip.go
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
699
v2/idiomatic/readerresult/flip_test.go
Normal file
699
v2/idiomatic/readerresult/flip_test.go
Normal 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
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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[
|
||||
|
||||
32
v2/io/io.go
32
v2/io/io.go
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
109
v2/json/json.go
109
v2/json/json.go
@@ -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, "", " "))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
32
v2/readerio/bracket.go
Normal 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,
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user