mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-15 23:33:46 +02:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16e82d6a65 | ||
|
|
0d40fdcebb | ||
|
|
6a4dfa2c93 | ||
|
|
a37f379a3c | ||
|
|
ece0cd135d | ||
|
|
739b6a284c | ||
|
|
ba10d8d314 | ||
|
|
3d6c419185 | ||
|
|
3f4b6292e4 | ||
|
|
b1704b6d26 | ||
|
|
ffdfd218f8 | ||
|
|
34826d8c52 | ||
|
|
24c0519cc7 | ||
|
|
ff48d8953e | ||
|
|
d739c9b277 | ||
|
|
f0054431a5 | ||
|
|
1a89ec3df7 | ||
|
|
f652a94c3a | ||
|
|
774db88ca5 | ||
|
|
62a3365b20 |
574
v2/DESIGN.md
Normal file
574
v2/DESIGN.md
Normal file
@@ -0,0 +1,574 @@
|
||||
# Design Decisions
|
||||
|
||||
This document explains the key design decisions and principles behind fp-go's API design.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Data Last Principle](#data-last-principle)
|
||||
- [Kleisli and Operator Types](#kleisli-and-operator-types)
|
||||
- [Monadic Operations Comparison](#monadic-operations-comparison)
|
||||
- [Type Parameter Ordering](#type-parameter-ordering)
|
||||
- [Generic Type Aliases](#generic-type-aliases)
|
||||
|
||||
## Data Last Principle
|
||||
|
||||
fp-go follows the **"data last"** principle, where the data being operated on is always the last parameter in a function. This design choice enables powerful function composition and partial application patterns.
|
||||
|
||||
### What is "Data Last"?
|
||||
|
||||
In the "data last" style, functions are structured so that:
|
||||
1. Configuration parameters come first
|
||||
2. The data to be transformed comes last
|
||||
|
||||
This is the opposite of the traditional object-oriented style where the data (receiver) comes first.
|
||||
|
||||
### Why "Data Last"?
|
||||
|
||||
The "data last" principle enables:
|
||||
|
||||
1. **Natural Currying**: Functions can be partially applied to create specialized transformations
|
||||
2. **Function Composition**: Operations can be composed before applying them to data
|
||||
3. **Point-Free Style**: Write transformations without explicitly mentioning the data
|
||||
4. **Reusability**: Create reusable transformation pipelines
|
||||
|
||||
### Examples
|
||||
|
||||
#### Basic Transformation
|
||||
|
||||
```go
|
||||
// Data last style (fp-go)
|
||||
double := array.Map(number.Mul(2))
|
||||
result := double([]int{1, 2, 3}) // [2, 4, 6]
|
||||
|
||||
// Compare with data first style (traditional)
|
||||
result := array.Map([]int{1, 2, 3}, number.Mul(2))
|
||||
```
|
||||
|
||||
#### Function Composition
|
||||
|
||||
```go
|
||||
import (
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
// Create a pipeline of transformations
|
||||
pipeline := F.Flow3(
|
||||
A.Filter(func(x int) bool { return x > 0 }), // Keep positive numbers
|
||||
A.Map(N.Mul(2)), // Double each number
|
||||
A.Reduce(func(acc, x int) int { return acc + x }, 0), // Sum them up
|
||||
)
|
||||
|
||||
// Apply the pipeline to different data
|
||||
result1 := pipeline([]int{-1, 2, 3, -4, 5}) // (2 + 3 + 5) * 2 = 20
|
||||
result2 := pipeline([]int{1, 2, 3}) // (1 + 2 + 3) * 2 = 12
|
||||
```
|
||||
|
||||
#### Partial Application
|
||||
|
||||
```go
|
||||
import (
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// Create specialized functions by partial application
|
||||
getOrZero := O.GetOrElse(func() int { return 0 })
|
||||
getOrEmpty := O.GetOrElse(func() string { return "" })
|
||||
|
||||
// Use them with different data
|
||||
value1 := getOrZero(O.Some(42)) // 42
|
||||
value2 := getOrZero(O.None[int]()) // 0
|
||||
|
||||
text1 := getOrEmpty(O.Some("hello")) // "hello"
|
||||
text2 := getOrEmpty(O.None[string]()) // ""
|
||||
```
|
||||
|
||||
#### Building Reusable Transformations
|
||||
|
||||
```go
|
||||
import (
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// Create a reusable validation pipeline
|
||||
type User struct {
|
||||
Name string
|
||||
Email string
|
||||
Age int
|
||||
}
|
||||
|
||||
validateAge := E.FromPredicate(
|
||||
func(u User) bool { return u.Age >= 18 },
|
||||
func(u User) error { return errors.New("must be 18 or older") },
|
||||
)
|
||||
|
||||
validateEmail := E.FromPredicate(
|
||||
func(u User) bool { return strings.Contains(u.Email, "@") },
|
||||
func(u User) error { return errors.New("invalid email") },
|
||||
)
|
||||
|
||||
// Compose validators
|
||||
validateUser := F.Flow2(
|
||||
validateAge,
|
||||
E.Chain(validateEmail),
|
||||
)
|
||||
|
||||
// Apply to different users
|
||||
result1 := validateUser(User{Name: "Alice", Email: "alice@example.com", Age: 25})
|
||||
result2 := validateUser(User{Name: "Bob", Email: "invalid", Age: 30})
|
||||
```
|
||||
|
||||
#### Monadic Operations
|
||||
|
||||
```go
|
||||
import (
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// Data last enables clean monadic chains
|
||||
parseAndDouble := F.Flow2(
|
||||
O.FromPredicate(func(s string) bool { return s != "" }),
|
||||
O.Chain(func(s string) O.Option[int] {
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return O.None[int]()
|
||||
}
|
||||
return O.Some(n * 2)
|
||||
}),
|
||||
)
|
||||
|
||||
result1 := parseAndDouble("21") // Some(42)
|
||||
result2 := parseAndDouble("") // None
|
||||
result3 := parseAndDouble("abc") // None
|
||||
```
|
||||
|
||||
### Monadic vs Non-Monadic Forms
|
||||
|
||||
fp-go provides two forms for most operations:
|
||||
|
||||
1. **Curried form** (data last): Returns a function that can be composed
|
||||
2. **Monadic form** (data first): Takes all parameters at once
|
||||
|
||||
```go
|
||||
// Curried form - data last, returns a function
|
||||
Map[A, B any](f func(A) B) func(Option[A]) Option[B]
|
||||
|
||||
// Monadic form - data first, direct execution
|
||||
MonadMap[A, B any](fa Option[A], f func(A) B) Option[B]
|
||||
```
|
||||
|
||||
**When to use each:**
|
||||
|
||||
- **Curried form**: When building pipelines, composing functions, or creating reusable transformations
|
||||
- **Monadic form**: When you have all parameters available and want direct execution
|
||||
|
||||
```go
|
||||
// Curried form - building a pipeline
|
||||
transform := F.Flow3(
|
||||
O.Map(strings.ToUpper),
|
||||
O.Filter(func(s string) bool { return len(s) > 3 }),
|
||||
O.GetOrElse(func() string { return "DEFAULT" }),
|
||||
)
|
||||
result := transform(O.Some("hello"))
|
||||
|
||||
// Monadic form - direct execution
|
||||
result := O.MonadMap(O.Some("hello"), strings.ToUpper)
|
||||
```
|
||||
|
||||
### Further Reading on Data-Last Pattern
|
||||
|
||||
The data-last currying pattern is well-documented in the functional programming community:
|
||||
|
||||
- [Mostly Adequate Guide - Ch. 4: Currying](https://mostly-adequate.gitbook.io/mostly-adequate-guide/ch04) - Excellent introduction with clear examples
|
||||
- [Curry and Function Composition](https://medium.com/javascript-scene/curry-and-function-composition-2c208d774983) by Eric Elliott
|
||||
- [fp-ts Issue #1238](https://github.com/gcanti/fp-ts/issues/1238) - Real-world examples of data-last refactoring
|
||||
|
||||
## Kleisli and Operator Types
|
||||
|
||||
fp-go uses consistent type aliases across all monads to make code more recognizable and composable. These types provide a common vocabulary that works across different monadic contexts.
|
||||
|
||||
### Type Definitions
|
||||
|
||||
```go
|
||||
// Kleisli arrow - a function that returns a monadic value
|
||||
type Kleisli[A, B any] = func(A) M[B]
|
||||
|
||||
// Operator - a function that transforms a monadic value
|
||||
type Operator[A, B any] = func(M[A]) M[B]
|
||||
```
|
||||
|
||||
Where `M` represents the specific monad (Option, Either, IO, etc.).
|
||||
|
||||
### Why These Types Matter
|
||||
|
||||
1. **Consistency**: The same type names appear across all monads
|
||||
2. **Recognizability**: Experienced functional programmers immediately understand the intent
|
||||
3. **Composability**: Functions with these types compose naturally
|
||||
4. **Documentation**: Type signatures clearly communicate the operation's behavior
|
||||
|
||||
### Examples Across Monads
|
||||
|
||||
#### Option Monad
|
||||
|
||||
```go
|
||||
// option/option.go
|
||||
type Kleisli[A, B any] = func(A) Option[B]
|
||||
type Operator[A, B any] = func(Option[A]) Option[B]
|
||||
|
||||
// Chain uses Kleisli
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B]
|
||||
|
||||
// Map returns an Operator
|
||||
func Map[A, B any](f func(A) B) Operator[A, B]
|
||||
```
|
||||
|
||||
#### Either Monad
|
||||
|
||||
```go
|
||||
// either/either.go
|
||||
type Kleisli[E, A, B any] = func(A) Either[E, B]
|
||||
type Operator[E, A, B any] = func(Either[E, A]) Either[E, B]
|
||||
|
||||
// Chain uses Kleisli
|
||||
func Chain[E, A, B any](f Kleisli[E, A, B]) Operator[E, A, B]
|
||||
|
||||
// Map returns an Operator
|
||||
func Map[E, A, B any](f func(A) B) Operator[E, A, B]
|
||||
```
|
||||
|
||||
#### IO Monad
|
||||
|
||||
```go
|
||||
// io/io.go
|
||||
type Kleisli[A, B any] = func(A) IO[B]
|
||||
type Operator[A, B any] = func(IO[A]) IO[B]
|
||||
|
||||
// Chain uses Kleisli
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B]
|
||||
|
||||
// Map returns an Operator
|
||||
func Map[A, B any](f func(A) B) Operator[A, B]
|
||||
```
|
||||
|
||||
#### Array (List Monad)
|
||||
|
||||
```go
|
||||
// array/array.go
|
||||
type Kleisli[A, B any] = func(A) []B
|
||||
type Operator[A, B any] = func([]A) []B
|
||||
|
||||
// Chain uses Kleisli
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B]
|
||||
|
||||
// Map returns an Operator
|
||||
func Map[A, B any](f func(A) B) Operator[A, B]
|
||||
```
|
||||
|
||||
### Pattern Recognition
|
||||
|
||||
Once you learn these patterns in one monad, you can apply them to all monads:
|
||||
|
||||
```go
|
||||
// The pattern is always the same, just the monad changes
|
||||
|
||||
// Option
|
||||
validateAge := option.Chain(func(user User) option.Option[User] {
|
||||
if user.Age >= 18 {
|
||||
return option.Some(user)
|
||||
}
|
||||
return option.None[User]()
|
||||
})
|
||||
|
||||
// Either
|
||||
validateAge := either.Chain(func(user User) either.Either[error, User] {
|
||||
if user.Age >= 18 {
|
||||
return either.Right[error](user)
|
||||
}
|
||||
return either.Left[User](errors.New("too young"))
|
||||
})
|
||||
|
||||
// IO
|
||||
validateAge := io.Chain(func(user User) io.IO[User] {
|
||||
return io.Of(user) // Always succeeds in IO
|
||||
})
|
||||
|
||||
// Array
|
||||
validateAge := array.Chain(func(user User) []User {
|
||||
if user.Age >= 18 {
|
||||
return []User{user}
|
||||
}
|
||||
return []User{} // Empty array = failure
|
||||
})
|
||||
```
|
||||
|
||||
### Composing Kleisli Arrows
|
||||
|
||||
Kleisli arrows compose naturally using monadic composition:
|
||||
|
||||
```go
|
||||
import (
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
// Define Kleisli arrows
|
||||
parseAge := func(s string) O.Option[int] {
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return O.None[int]()
|
||||
}
|
||||
return O.Some(n)
|
||||
}
|
||||
|
||||
validateAge := func(age int) O.Option[int] {
|
||||
if age >= 18 {
|
||||
return O.Some(age)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
|
||||
formatAge := func(age int) O.Option[string] {
|
||||
return O.Some(fmt.Sprintf("Age: %d", age))
|
||||
}
|
||||
|
||||
// Compose them using Flow and Chain
|
||||
pipeline := F.Flow3(
|
||||
parseAge,
|
||||
O.Chain(validateAge),
|
||||
O.Chain(formatAge),
|
||||
)
|
||||
|
||||
result := pipeline("25") // Some("Age: 25")
|
||||
result := pipeline("15") // None (too young)
|
||||
result := pipeline("abc") // None (parse error)
|
||||
```
|
||||
|
||||
### Building Reusable Operators
|
||||
|
||||
Operators can be created once and reused across your codebase:
|
||||
|
||||
```go
|
||||
import (
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
)
|
||||
|
||||
// Create reusable operators
|
||||
type ValidationError struct {
|
||||
Field string
|
||||
Message string
|
||||
}
|
||||
|
||||
// Reusable validation operators
|
||||
validateNonEmpty := E.Chain(func(s string) E.Either[ValidationError, string] {
|
||||
if s == "" {
|
||||
return E.Left[string](ValidationError{
|
||||
Field: "input",
|
||||
Message: "cannot be empty",
|
||||
})
|
||||
}
|
||||
return E.Right[ValidationError](s)
|
||||
})
|
||||
|
||||
validateEmail := E.Chain(func(s string) E.Either[ValidationError, string] {
|
||||
if !strings.Contains(s, "@") {
|
||||
return E.Left[string](ValidationError{
|
||||
Field: "email",
|
||||
Message: "invalid format",
|
||||
})
|
||||
}
|
||||
return E.Right[ValidationError](s)
|
||||
})
|
||||
|
||||
// Compose operators
|
||||
validateEmailInput := F.Flow2(
|
||||
validateNonEmpty,
|
||||
validateEmail,
|
||||
)
|
||||
|
||||
// Use across your application
|
||||
result1 := validateEmailInput(E.Right[ValidationError]("user@example.com"))
|
||||
result2 := validateEmailInput(E.Right[ValidationError](""))
|
||||
result3 := validateEmailInput(E.Right[ValidationError]("invalid"))
|
||||
```
|
||||
|
||||
### Benefits of Consistent Naming
|
||||
|
||||
1. **Cross-monad understanding**: Learn once, apply everywhere
|
||||
2. **Easier refactoring**: Changing monads requires minimal code changes
|
||||
3. **Better tooling**: IDEs can provide better suggestions
|
||||
4. **Team communication**: Shared vocabulary across the team
|
||||
5. **Library integration**: Third-party libraries follow the same patterns
|
||||
|
||||
### Identity Monad - The Simplest Case
|
||||
|
||||
The Identity monad shows these types in their simplest form:
|
||||
|
||||
```go
|
||||
// identity/doc.go
|
||||
type Operator[A, B any] = func(A) B
|
||||
|
||||
// In Identity, there's no wrapping, so:
|
||||
// - Kleisli[A, B] is just func(A) B
|
||||
// - Operator[A, B] is just func(A) B
|
||||
// They're the same because Identity adds no context
|
||||
```
|
||||
|
||||
This demonstrates that these type aliases represent fundamental functional programming concepts, not just arbitrary naming conventions.
|
||||
|
||||
|
||||
## Monadic Operations Comparison
|
||||
|
||||
fp-go's monadic operations are inspired by functional programming languages and libraries. Here's how they compare:
|
||||
|
||||
| fp-go | fp-ts | Haskell | Scala | Description |
|
||||
|-------|-------|---------|-------|-------------|
|
||||
| `Map` | `map` | `fmap` | `map` | Functor mapping - transforms the value inside a context |
|
||||
| `Chain` | `chain` | `>>=` (bind) | `flatMap` | Monadic bind - chains computations that return wrapped values |
|
||||
| `Ap` | `ap` | `<*>` | `ap` | Applicative apply - applies a wrapped function to a wrapped value |
|
||||
| `Of` | `of` | `return`/`pure` | `pure` | Lifts a pure value into a monadic context |
|
||||
| `Fold` | `fold` | `either` | `fold` | Eliminates the context by providing handlers for each case |
|
||||
| `Filter` | `filter` | `mfilter` | `filter` | Keeps values that satisfy a predicate |
|
||||
| `Flatten` | `flatten` | `join` | `flatten` | Removes one level of nesting |
|
||||
| `ChainFirst` | `chainFirst` | `>>` (then) | `tap` | Chains for side effects, keeping the original value |
|
||||
| `Alt` | `alt` | `<\|>` | `orElse` | Provides an alternative value if the first fails |
|
||||
| `GetOrElse` | `getOrElse` | `fromMaybe` | `getOrElse` | Extracts the value or provides a default |
|
||||
| `FromPredicate` | `fromPredicate` | `guard` | `filter` | Creates a monadic value based on a predicate |
|
||||
| `Sequence` | `sequence` | `sequence` | `sequence` | Transforms a collection of effects into an effect of a collection |
|
||||
| `Traverse` | `traverse` | `traverse` | `traverse` | Maps and sequences in one operation |
|
||||
| `Reduce` | `reduce` | `foldl` | `foldLeft` | Folds a structure from left to right |
|
||||
| `ReduceRight` | `reduceRight` | `foldr` | `foldRight` | Folds a structure from right to left |
|
||||
|
||||
### Key Differences from Other Languages
|
||||
|
||||
#### Naming Conventions
|
||||
|
||||
- **Go conventions**: fp-go uses PascalCase for exported functions (e.g., `Map`, `Chain`) following Go's naming conventions
|
||||
- **Type parameters first**: Non-inferrable type parameters come first (e.g., `Ap[B, E, A any]`)
|
||||
- **Monadic prefix**: Direct execution forms use the `Monad` prefix (e.g., `MonadMap`, `MonadChain`)
|
||||
|
||||
#### Type System
|
||||
|
||||
```go
|
||||
// fp-go (explicit type parameters when needed)
|
||||
result := option.Map(transform)(value)
|
||||
result := option.Map[string, int](transform)(value) // explicit when inference fails
|
||||
|
||||
// Haskell (type inference)
|
||||
result = fmap transform value
|
||||
|
||||
// Scala (type inference with method syntax)
|
||||
result = value.map(transform)
|
||||
|
||||
// fp-ts (TypeScript type inference)
|
||||
const result = pipe(value, map(transform))
|
||||
```
|
||||
|
||||
#### Currying
|
||||
|
||||
```go
|
||||
// fp-go - explicit currying with data last
|
||||
double := array.Map(number.Mul(2))
|
||||
result := double(numbers)
|
||||
|
||||
// Haskell - automatic currying
|
||||
double = fmap (*2)
|
||||
result = double numbers
|
||||
|
||||
// Scala - method syntax
|
||||
result = numbers.map(_ * 2)
|
||||
```
|
||||
|
||||
## Type Parameter Ordering
|
||||
|
||||
fp-go v2 uses a specific ordering for type parameters to maximize type inference:
|
||||
|
||||
### Rule: Non-Inferrable Parameters First
|
||||
|
||||
Type parameters that **cannot be inferred** from function arguments come first. This allows the Go compiler to infer as many types as possible.
|
||||
|
||||
```go
|
||||
// Ap - B cannot be inferred from arguments, so it comes first
|
||||
func Ap[B, E, A any](fa Either[E, A]) func(Either[E, func(A) B]) Either[E, B]
|
||||
|
||||
// Usage - only B needs to be specified
|
||||
result := either.Ap[string](value)(funcInEither)
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```go
|
||||
// Map - all types can be inferred from arguments
|
||||
func Map[E, A, B any](f func(A) B) func(Either[E, A]) Either[E, B]
|
||||
// Usage - no type parameters needed
|
||||
result := either.Map(transform)(value)
|
||||
|
||||
// Chain - all types can be inferred
|
||||
func Chain[E, A, B any](f func(A) Either[E, B]) func(Either[E, A]) Either[E, B]
|
||||
// Usage - no type parameters needed
|
||||
result := either.Chain(validator)(value)
|
||||
|
||||
// Of - E cannot be inferred, comes first
|
||||
func Of[E, A any](value A) Either[E, A]
|
||||
// Usage - only E needs to be specified
|
||||
result := either.Of[error](42)
|
||||
```
|
||||
|
||||
### Benefits
|
||||
|
||||
1. **Less verbose code**: Most operations don't require explicit type parameters
|
||||
2. **Better IDE support**: Type inference provides better autocomplete
|
||||
3. **Clearer intent**: Only specify types that can't be inferred
|
||||
|
||||
## Generic Type Aliases
|
||||
|
||||
fp-go v2 leverages Go 1.24's generic type aliases for cleaner type definitions:
|
||||
|
||||
```go
|
||||
// V2 - using generic type alias (requires Go 1.24+)
|
||||
type ReaderIOEither[R, E, A any] = RD.Reader[R, IOE.IOEither[E, A]]
|
||||
|
||||
// V1 - using type definition (Go 1.18+)
|
||||
type ReaderIOEither[R, E, A any] RD.Reader[R, IOE.IOEither[E, A]]
|
||||
```
|
||||
|
||||
### Benefits
|
||||
|
||||
1. **True aliases**: The type is interchangeable with its definition
|
||||
2. **No namespace imports needed**: Can use types directly without package prefixes
|
||||
3. **Simpler codebase**: Eliminates the need for `generic` subpackages
|
||||
4. **Better composability**: Types compose more naturally
|
||||
|
||||
### Migration Pattern
|
||||
|
||||
```go
|
||||
// Define project-wide aliases once
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
)
|
||||
|
||||
type Option[A any] = option.Option[A]
|
||||
type Result[A any] = result.Result[A]
|
||||
type IOResult[A any] = ioresult.IOResult[A]
|
||||
|
||||
// Use throughout your codebase
|
||||
package myapp
|
||||
|
||||
import "myproject/types"
|
||||
|
||||
func process(input string) types.Result[types.Option[int]] {
|
||||
// implementation
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
For more information, see:
|
||||
- [README.md](./README.md) - Overview and quick start
|
||||
- [API Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2) - Complete API reference
|
||||
- [Samples](./samples/) - Practical examples
|
||||
@@ -19,6 +19,58 @@
|
||||
// allowing for composable and functional test assertions. Each assertion
|
||||
// returns a Reader that takes a *testing.T and performs the assertion.
|
||||
//
|
||||
// # Data Last Principle
|
||||
//
|
||||
// This package follows the "data last" functional programming principle, where
|
||||
// the data being operated on comes as the last parameter in a chain of function
|
||||
// applications. This design enables several powerful functional programming patterns:
|
||||
//
|
||||
// 1. **Partial Application**: You can create reusable assertion functions by providing
|
||||
// configuration parameters first, leaving the data and testing context for later.
|
||||
//
|
||||
// 2. **Function Composition**: Assertions can be composed and combined before being
|
||||
// applied to actual data.
|
||||
//
|
||||
// 3. **Point-Free Style**: You can pass assertion functions around without immediately
|
||||
// providing the data they operate on.
|
||||
//
|
||||
// The general pattern is:
|
||||
//
|
||||
// assert.Function(config)(data)(testingContext)
|
||||
// ↑ ↑ ↑
|
||||
// expected actual *testing.T (always last)
|
||||
//
|
||||
// For single-parameter assertions:
|
||||
//
|
||||
// assert.Function(data)(testingContext)
|
||||
// ↑ ↑
|
||||
// actual *testing.T (always last)
|
||||
//
|
||||
// Examples of "data last" in action:
|
||||
//
|
||||
// // Multi-parameter: expected value → actual value → testing context
|
||||
// assert.Equal(42)(result)(t)
|
||||
// assert.ArrayContains(3)(numbers)(t)
|
||||
//
|
||||
// // Single-parameter: data → testing context
|
||||
// assert.NoError(err)(t)
|
||||
// assert.ArrayNotEmpty(arr)(t)
|
||||
//
|
||||
// // Partial application - create reusable assertions
|
||||
// isPositive := assert.That(func(n int) bool { return n > 0 })
|
||||
// // Later, apply to different values:
|
||||
// isPositive(42)(t) // Passes
|
||||
// isPositive(-5)(t) // Fails
|
||||
//
|
||||
// // Composition - combine assertions before applying data
|
||||
// validateUser := func(u User) assert.Reader {
|
||||
// return assert.AllOf([]assert.Reader{
|
||||
// assert.Equal("Alice")(u.Name),
|
||||
// assert.That(func(age int) bool { return age >= 18 })(u.Age),
|
||||
// })
|
||||
// }
|
||||
// validateUser(user)(t)
|
||||
//
|
||||
// The package supports:
|
||||
// - Equality and inequality assertions
|
||||
// - Collection assertions (arrays, maps, strings)
|
||||
@@ -83,38 +135,108 @@ func wrap1[T any](wrapped func(t assert.TestingT, expected, actual any, msgAndAr
|
||||
}
|
||||
}
|
||||
|
||||
// NotEqual tests if the expected and the actual values are not equal
|
||||
// NotEqual tests if the expected and the actual values are not equal.
|
||||
//
|
||||
// This function follows the "data last" principle - you provide the expected value first,
|
||||
// then the actual value, and finally the testing.T context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestNotEqual(t *testing.T) {
|
||||
// value := 42
|
||||
// assert.NotEqual(10)(value)(t) // Passes: 42 != 10
|
||||
// assert.NotEqual(42)(value)(t) // Fails: 42 == 42
|
||||
// }
|
||||
func NotEqual[T any](expected T) Kleisli[T] {
|
||||
return wrap1(assert.NotEqual, expected)
|
||||
}
|
||||
|
||||
// Equal tests if the expected and the actual values are equal
|
||||
// Equal tests if the expected and the actual values are equal.
|
||||
//
|
||||
// This is one of the most commonly used assertions. It follows the "data last" principle -
|
||||
// you provide the expected value first, then the actual value, and finally the testing.T context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestEqual(t *testing.T) {
|
||||
// result := 2 + 2
|
||||
// assert.Equal(4)(result)(t) // Passes
|
||||
//
|
||||
// name := "Alice"
|
||||
// assert.Equal("Alice")(name)(t) // Passes
|
||||
//
|
||||
// // Can be composed with other assertions
|
||||
// user := User{Name: "Bob", Age: 30}
|
||||
// assertions := assert.AllOf([]assert.Reader{
|
||||
// assert.Equal("Bob")(user.Name),
|
||||
// assert.Equal(30)(user.Age),
|
||||
// })
|
||||
// assertions(t)
|
||||
// }
|
||||
func Equal[T any](expected T) Kleisli[T] {
|
||||
return wrap1(assert.Equal, expected)
|
||||
}
|
||||
|
||||
// ArrayNotEmpty checks if an array is not empty
|
||||
// ArrayNotEmpty checks if an array is not empty.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestArrayNotEmpty(t *testing.T) {
|
||||
// numbers := []int{1, 2, 3}
|
||||
// assert.ArrayNotEmpty(numbers)(t) // Passes
|
||||
//
|
||||
// empty := []int{}
|
||||
// assert.ArrayNotEmpty(empty)(t) // Fails
|
||||
// }
|
||||
func ArrayNotEmpty[T any](arr []T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.NotEmpty(t, arr)
|
||||
}
|
||||
}
|
||||
|
||||
// RecordNotEmpty checks if an map is not empty
|
||||
// RecordNotEmpty checks if a map is not empty.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestRecordNotEmpty(t *testing.T) {
|
||||
// config := map[string]int{"timeout": 30, "retries": 3}
|
||||
// assert.RecordNotEmpty(config)(t) // Passes
|
||||
//
|
||||
// empty := map[string]int{}
|
||||
// assert.RecordNotEmpty(empty)(t) // Fails
|
||||
// }
|
||||
func RecordNotEmpty[K comparable, T any](mp map[K]T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.NotEmpty(t, mp)
|
||||
}
|
||||
}
|
||||
|
||||
// StringNotEmpty checks if a string is not empty
|
||||
// StringNotEmpty checks if a string is not empty.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestStringNotEmpty(t *testing.T) {
|
||||
// message := "Hello, World!"
|
||||
// assert.StringNotEmpty(message)(t) // Passes
|
||||
//
|
||||
// empty := ""
|
||||
// assert.StringNotEmpty(empty)(t) // Fails
|
||||
// }
|
||||
func StringNotEmpty(s string) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.NotEmpty(t, s)
|
||||
}
|
||||
}
|
||||
|
||||
// ArrayLength tests if an array has the expected length
|
||||
// ArrayLength tests if an array has the expected length.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestArrayLength(t *testing.T) {
|
||||
// numbers := []int{1, 2, 3, 4, 5}
|
||||
// assert.ArrayLength[int](5)(numbers)(t) // Passes
|
||||
// assert.ArrayLength[int](3)(numbers)(t) // Fails
|
||||
// }
|
||||
func ArrayLength[T any](expected int) Kleisli[[]T] {
|
||||
return func(actual []T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
@@ -123,7 +245,15 @@ func ArrayLength[T any](expected int) Kleisli[[]T] {
|
||||
}
|
||||
}
|
||||
|
||||
// RecordLength tests if a map has the expected length
|
||||
// RecordLength tests if a map has the expected length.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestRecordLength(t *testing.T) {
|
||||
// config := map[string]string{"host": "localhost", "port": "8080"}
|
||||
// assert.RecordLength[string, string](2)(config)(t) // Passes
|
||||
// assert.RecordLength[string, string](3)(config)(t) // Fails
|
||||
// }
|
||||
func RecordLength[K comparable, T any](expected int) Kleisli[map[K]T] {
|
||||
return func(actual map[K]T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
@@ -132,7 +262,15 @@ func RecordLength[K comparable, T any](expected int) Kleisli[map[K]T] {
|
||||
}
|
||||
}
|
||||
|
||||
// StringLength tests if a string has the expected length
|
||||
// StringLength tests if a string has the expected length.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestStringLength(t *testing.T) {
|
||||
// message := "Hello"
|
||||
// assert.StringLength[any, any](5)(message)(t) // Passes
|
||||
// assert.StringLength[any, any](10)(message)(t) // Fails
|
||||
// }
|
||||
func StringLength[K comparable, T any](expected int) Kleisli[string] {
|
||||
return func(actual string) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
@@ -141,31 +279,93 @@ func StringLength[K comparable, T any](expected int) Kleisli[string] {
|
||||
}
|
||||
}
|
||||
|
||||
// NoError validates that there is no error
|
||||
// NoError validates that there is no error.
|
||||
//
|
||||
// This is commonly used to assert that operations complete successfully.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestNoError(t *testing.T) {
|
||||
// err := doSomething()
|
||||
// assert.NoError(err)(t) // Passes if err is nil
|
||||
//
|
||||
// // Can be used with result types
|
||||
// result := result.TryCatch(func() (int, error) {
|
||||
// return 42, nil
|
||||
// })
|
||||
// assert.Success(result)(t) // Uses NoError internally
|
||||
// }
|
||||
func NoError(err error) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Error validates that there is an error
|
||||
// Error validates that there is an error.
|
||||
//
|
||||
// This is used to assert that operations fail as expected.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestError(t *testing.T) {
|
||||
// err := validateInput("")
|
||||
// assert.Error(err)(t) // Passes if err is not nil
|
||||
//
|
||||
// err2 := validateInput("valid")
|
||||
// assert.Error(err2)(t) // Fails if err2 is nil
|
||||
// }
|
||||
func Error(err error) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Success checks if a [Result] represents success
|
||||
// Success checks if a [Result] represents success.
|
||||
//
|
||||
// This is a convenience function for testing Result types from the fp-go library.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestSuccess(t *testing.T) {
|
||||
// res := result.Of[int](42)
|
||||
// assert.Success(res)(t) // Passes
|
||||
//
|
||||
// failedRes := result.Error[int](errors.New("failed"))
|
||||
// assert.Success(failedRes)(t) // Fails
|
||||
// }
|
||||
func Success[T any](res Result[T]) Reader {
|
||||
return NoError(result.ToError(res))
|
||||
}
|
||||
|
||||
// Failure checks if a [Result] represents failure
|
||||
// Failure checks if a [Result] represents failure.
|
||||
//
|
||||
// This is a convenience function for testing Result types from the fp-go library.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestFailure(t *testing.T) {
|
||||
// res := result.Error[int](errors.New("something went wrong"))
|
||||
// assert.Failure(res)(t) // Passes
|
||||
//
|
||||
// successRes := result.Of[int](42)
|
||||
// assert.Failure(successRes)(t) // Fails
|
||||
// }
|
||||
func Failure[T any](res Result[T]) Reader {
|
||||
return Error(result.ToError(res))
|
||||
}
|
||||
|
||||
// ArrayContains tests if a value is contained in an array
|
||||
// ArrayContains tests if a value is contained in an array.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestArrayContains(t *testing.T) {
|
||||
// numbers := []int{1, 2, 3, 4, 5}
|
||||
// assert.ArrayContains(3)(numbers)(t) // Passes
|
||||
// assert.ArrayContains(10)(numbers)(t) // Fails
|
||||
//
|
||||
// names := []string{"Alice", "Bob", "Charlie"}
|
||||
// assert.ArrayContains("Bob")(names)(t) // Passes
|
||||
// }
|
||||
func ArrayContains[T any](expected T) Kleisli[[]T] {
|
||||
return func(actual []T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
@@ -174,7 +374,15 @@ func ArrayContains[T any](expected T) Kleisli[[]T] {
|
||||
}
|
||||
}
|
||||
|
||||
// ContainsKey tests if a key is contained in a map
|
||||
// ContainsKey tests if a key is contained in a map.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestContainsKey(t *testing.T) {
|
||||
// config := map[string]int{"timeout": 30, "retries": 3}
|
||||
// assert.ContainsKey[int]("timeout")(config)(t) // Passes
|
||||
// assert.ContainsKey[int]("maxSize")(config)(t) // Fails
|
||||
// }
|
||||
func ContainsKey[T any, K comparable](expected K) Kleisli[map[K]T] {
|
||||
return func(actual map[K]T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
@@ -183,7 +391,15 @@ func ContainsKey[T any, K comparable](expected K) Kleisli[map[K]T] {
|
||||
}
|
||||
}
|
||||
|
||||
// NotContainsKey tests if a key is not contained in a map
|
||||
// NotContainsKey tests if a key is not contained in a map.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestNotContainsKey(t *testing.T) {
|
||||
// config := map[string]int{"timeout": 30, "retries": 3}
|
||||
// assert.NotContainsKey[int]("maxSize")(config)(t) // Passes
|
||||
// assert.NotContainsKey[int]("timeout")(config)(t) // Fails
|
||||
// }
|
||||
func NotContainsKey[T any, K comparable](expected K) Kleisli[map[K]T] {
|
||||
return func(actual map[K]T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
@@ -192,7 +408,31 @@ func NotContainsKey[T any, K comparable](expected K) Kleisli[map[K]T] {
|
||||
}
|
||||
}
|
||||
|
||||
// That asserts that a particular predicate matches
|
||||
// That asserts that a particular predicate matches.
|
||||
//
|
||||
// This is a powerful function that allows you to create custom assertions using predicates.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestThat(t *testing.T) {
|
||||
// // Test if a number is positive
|
||||
// isPositive := func(n int) bool { return n > 0 }
|
||||
// assert.That(isPositive)(42)(t) // Passes
|
||||
// assert.That(isPositive)(-5)(t) // Fails
|
||||
//
|
||||
// // Test if a string is uppercase
|
||||
// isUppercase := func(s string) bool { return s == strings.ToUpper(s) }
|
||||
// assert.That(isUppercase)("HELLO")(t) // Passes
|
||||
// assert.That(isUppercase)("Hello")(t) // Fails
|
||||
//
|
||||
// // Can be combined with Local for property testing
|
||||
// type User struct { Age int }
|
||||
// ageIsAdult := assert.Local(func(u User) int { return u.Age })(
|
||||
// assert.That(func(age int) bool { return age >= 18 }),
|
||||
// )
|
||||
// user := User{Age: 25}
|
||||
// ageIsAdult(user)(t) // Passes
|
||||
// }
|
||||
func That[T any](pred Predicate[T]) Kleisli[T] {
|
||||
return func(a T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
|
||||
235
v2/assert/example_test.go
Normal file
235
v2/assert/example_test.go
Normal file
@@ -0,0 +1,235 @@
|
||||
// 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 assert_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/assert"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// Example_basicAssertions demonstrates basic equality and inequality assertions
|
||||
func Example_basicAssertions() {
|
||||
// This would be in a real test function
|
||||
var t *testing.T // placeholder for example
|
||||
|
||||
// Basic equality
|
||||
value := 42
|
||||
assert.Equal(42)(value)(t)
|
||||
|
||||
// String equality
|
||||
name := "Alice"
|
||||
assert.Equal("Alice")(name)(t)
|
||||
|
||||
// Inequality
|
||||
assert.NotEqual(10)(value)(t)
|
||||
}
|
||||
|
||||
// Example_arrayAssertions demonstrates array-related assertions
|
||||
func Example_arrayAssertions() {
|
||||
var t *testing.T // placeholder for example
|
||||
|
||||
numbers := []int{1, 2, 3, 4, 5}
|
||||
|
||||
// Check array is not empty
|
||||
assert.ArrayNotEmpty(numbers)(t)
|
||||
|
||||
// Check array length
|
||||
assert.ArrayLength[int](5)(numbers)(t)
|
||||
|
||||
// Check array contains a value
|
||||
assert.ArrayContains(3)(numbers)(t)
|
||||
}
|
||||
|
||||
// Example_mapAssertions demonstrates map-related assertions
|
||||
func Example_mapAssertions() {
|
||||
var t *testing.T // placeholder for example
|
||||
|
||||
config := map[string]int{
|
||||
"timeout": 30,
|
||||
"retries": 3,
|
||||
"maxSize": 1000,
|
||||
}
|
||||
|
||||
// Check map is not empty
|
||||
assert.RecordNotEmpty(config)(t)
|
||||
|
||||
// Check map length
|
||||
assert.RecordLength[string, int](3)(config)(t)
|
||||
|
||||
// Check map contains key
|
||||
assert.ContainsKey[int]("timeout")(config)(t)
|
||||
|
||||
// Check map does not contain key
|
||||
assert.NotContainsKey[int]("unknown")(config)(t)
|
||||
}
|
||||
|
||||
// Example_errorAssertions demonstrates error-related assertions
|
||||
func Example_errorAssertions() {
|
||||
var t *testing.T // placeholder for example
|
||||
|
||||
// Assert no error
|
||||
err := doSomethingSuccessful()
|
||||
assert.NoError(err)(t)
|
||||
|
||||
// Assert error exists
|
||||
err2 := doSomethingThatFails()
|
||||
assert.Error(err2)(t)
|
||||
}
|
||||
|
||||
// Example_resultAssertions demonstrates Result type assertions
|
||||
func Example_resultAssertions() {
|
||||
var t *testing.T // placeholder for example
|
||||
|
||||
// Assert success
|
||||
successResult := result.Of[int](42)
|
||||
assert.Success(successResult)(t)
|
||||
|
||||
// Assert failure
|
||||
failureResult := result.Left[int](errors.New("something went wrong"))
|
||||
assert.Failure(failureResult)(t)
|
||||
}
|
||||
|
||||
// Example_predicateAssertions demonstrates custom predicate assertions
|
||||
func Example_predicateAssertions() {
|
||||
var t *testing.T // placeholder for example
|
||||
|
||||
// Test if a number is positive
|
||||
isPositive := func(n int) bool { return n > 0 }
|
||||
assert.That(isPositive)(42)(t)
|
||||
|
||||
// Test if a string is uppercase
|
||||
isUppercase := func(s string) bool { return s == strings.ToUpper(s) }
|
||||
assert.That(isUppercase)("HELLO")(t)
|
||||
|
||||
// Test if a number is even
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
assert.That(isEven)(10)(t)
|
||||
}
|
||||
|
||||
// Example_allOf demonstrates combining multiple assertions
|
||||
func Example_allOf() {
|
||||
var t *testing.T // placeholder for example
|
||||
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Active bool
|
||||
}
|
||||
|
||||
user := User{Name: "Alice", Age: 30, Active: true}
|
||||
|
||||
// Combine multiple assertions
|
||||
assertions := assert.AllOf([]assert.Reader{
|
||||
assert.Equal("Alice")(user.Name),
|
||||
assert.Equal(30)(user.Age),
|
||||
assert.Equal(true)(user.Active),
|
||||
})
|
||||
|
||||
assertions(t)
|
||||
}
|
||||
|
||||
// Example_runAll demonstrates running named test cases
|
||||
func Example_runAll() {
|
||||
var t *testing.T // placeholder for example
|
||||
|
||||
testcases := map[string]assert.Reader{
|
||||
"addition": assert.Equal(4)(2 + 2),
|
||||
"multiplication": assert.Equal(6)(2 * 3),
|
||||
"subtraction": assert.Equal(1)(3 - 2),
|
||||
"division": assert.Equal(2)(10 / 5),
|
||||
}
|
||||
|
||||
assert.RunAll(testcases)(t)
|
||||
}
|
||||
|
||||
// Example_local demonstrates focusing assertions on specific properties
|
||||
func Example_local() {
|
||||
var t *testing.T // placeholder for example
|
||||
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
// Create an assertion that checks if age is positive
|
||||
ageIsPositive := assert.That(func(age int) bool { return age > 0 })
|
||||
|
||||
// Focus this assertion on the Age field of User
|
||||
userAgeIsPositive := assert.Local(func(u User) int { return u.Age })(ageIsPositive)
|
||||
|
||||
// Now we can test the whole User object
|
||||
user := User{Name: "Alice", Age: 30}
|
||||
userAgeIsPositive(user)(t)
|
||||
}
|
||||
|
||||
// Example_composableAssertions demonstrates building complex assertions
|
||||
func Example_composableAssertions() {
|
||||
var t *testing.T // placeholder for example
|
||||
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
Timeout int
|
||||
Retries int
|
||||
}
|
||||
|
||||
config := Config{
|
||||
Host: "localhost",
|
||||
Port: 8080,
|
||||
Timeout: 30,
|
||||
Retries: 3,
|
||||
}
|
||||
|
||||
// Create focused assertions for each field
|
||||
validHost := assert.Local(func(c Config) string { return c.Host })(
|
||||
assert.StringNotEmpty,
|
||||
)
|
||||
|
||||
validPort := assert.Local(func(c Config) int { return c.Port })(
|
||||
assert.That(func(p int) bool { return p > 0 && p < 65536 }),
|
||||
)
|
||||
|
||||
validTimeout := assert.Local(func(c Config) int { return c.Timeout })(
|
||||
assert.That(func(t int) bool { return t > 0 }),
|
||||
)
|
||||
|
||||
validRetries := assert.Local(func(c Config) int { return c.Retries })(
|
||||
assert.That(func(r int) bool { return r >= 0 }),
|
||||
)
|
||||
|
||||
// Combine all assertions
|
||||
validConfig := assert.AllOf([]assert.Reader{
|
||||
validHost(config),
|
||||
validPort(config),
|
||||
validTimeout(config),
|
||||
validRetries(config),
|
||||
})
|
||||
|
||||
validConfig(t)
|
||||
}
|
||||
|
||||
// Helper functions for examples
|
||||
func doSomethingSuccessful() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func doSomethingThatFails() error {
|
||||
return errors.New("operation failed")
|
||||
}
|
||||
@@ -8,5 +8,5 @@ import (
|
||||
|
||||
// BuilderPrism createa a [Prism] that converts between a builder and its type
|
||||
func BuilderPrism[T any, B Builder[T]](creator func(T) B) Prism[B, T] {
|
||||
return prism.MakePrism(F.Flow2(B.Build, result.ToOption[T]), creator)
|
||||
return prism.MakePrismWithName(F.Flow2(B.Build, result.ToOption[T]), creator, "BuilderPrism")
|
||||
}
|
||||
|
||||
177
v2/consumer/consumer.go
Normal file
177
v2/consumer/consumer.go
Normal file
@@ -0,0 +1,177 @@
|
||||
// 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 consumer
|
||||
|
||||
// Local transforms a Consumer by preprocessing its input through a function.
|
||||
// This is the contravariant map operation for Consumers, analogous to reader.Local
|
||||
// but operating on the input side rather than the output side.
|
||||
//
|
||||
// Given a Consumer[R1] that consumes values of type R1, and a function f that
|
||||
// converts R2 to R1, Local creates a new Consumer[R2] that:
|
||||
// 1. Takes a value of type R2
|
||||
// 2. Applies f to convert it to R1
|
||||
// 3. Passes the result to the original Consumer[R1]
|
||||
//
|
||||
// This is particularly useful for adapting consumers to work with different input types,
|
||||
// similar to how reader.Local adapts readers to work with different environment types.
|
||||
//
|
||||
// Comparison with reader.Local:
|
||||
// - reader.Local: Transforms the environment BEFORE passing it to a Reader (preprocessing input)
|
||||
// - consumer.Local: Transforms the value BEFORE passing it to a Consumer (preprocessing input)
|
||||
// - Both are contravariant operations on the input type
|
||||
// - Reader produces output, Consumer performs side effects
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R2: The input type of the new Consumer (what you have)
|
||||
// - R1: The input type of the original Consumer (what it expects)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that converts R2 to R1 (preprocessing function)
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms Consumer[R1] into Consumer[R2]
|
||||
//
|
||||
// Example - Basic type adaptation:
|
||||
//
|
||||
// // Consumer that logs integers
|
||||
// logInt := func(x int) {
|
||||
// fmt.Printf("Value: %d\n", x)
|
||||
// }
|
||||
//
|
||||
// // Adapt it to consume strings by parsing them first
|
||||
// parseToInt := func(s string) int {
|
||||
// n, _ := strconv.Atoi(s)
|
||||
// return n
|
||||
// }
|
||||
//
|
||||
// logString := consumer.Local(parseToInt)(logInt)
|
||||
// logString("42") // Logs: "Value: 42"
|
||||
//
|
||||
// Example - Extracting fields from structs:
|
||||
//
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// // Consumer that logs names
|
||||
// logName := func(name string) {
|
||||
// fmt.Printf("Name: %s\n", name)
|
||||
// }
|
||||
//
|
||||
// // Adapt it to consume User structs
|
||||
// extractName := func(u User) string {
|
||||
// return u.Name
|
||||
// }
|
||||
//
|
||||
// logUser := consumer.Local(extractName)(logName)
|
||||
// logUser(User{Name: "Alice", Age: 30}) // Logs: "Name: Alice"
|
||||
//
|
||||
// Example - Simplifying complex types:
|
||||
//
|
||||
// type DetailedConfig struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// Timeout time.Duration
|
||||
// MaxRetry int
|
||||
// }
|
||||
//
|
||||
// type SimpleConfig struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
//
|
||||
// // Consumer that logs simple configs
|
||||
// logSimple := func(c SimpleConfig) {
|
||||
// fmt.Printf("Server: %s:%d\n", c.Host, c.Port)
|
||||
// }
|
||||
//
|
||||
// // Adapt it to consume detailed configs
|
||||
// simplify := func(d DetailedConfig) SimpleConfig {
|
||||
// return SimpleConfig{Host: d.Host, Port: d.Port}
|
||||
// }
|
||||
//
|
||||
// logDetailed := consumer.Local(simplify)(logSimple)
|
||||
// logDetailed(DetailedConfig{
|
||||
// Host: "localhost",
|
||||
// Port: 8080,
|
||||
// Timeout: time.Second,
|
||||
// MaxRetry: 3,
|
||||
// }) // Logs: "Server: localhost:8080"
|
||||
//
|
||||
// Example - Composing multiple transformations:
|
||||
//
|
||||
// type Response struct {
|
||||
// StatusCode int
|
||||
// Body string
|
||||
// }
|
||||
//
|
||||
// // Consumer that logs status codes
|
||||
// logStatus := func(code int) {
|
||||
// fmt.Printf("Status: %d\n", code)
|
||||
// }
|
||||
//
|
||||
// // Extract status code from response
|
||||
// getStatus := func(r Response) int {
|
||||
// return r.StatusCode
|
||||
// }
|
||||
//
|
||||
// // Adapt to consume responses
|
||||
// logResponse := consumer.Local(getStatus)(logStatus)
|
||||
// logResponse(Response{StatusCode: 200, Body: "OK"}) // Logs: "Status: 200"
|
||||
//
|
||||
// Example - Using with multiple consumers:
|
||||
//
|
||||
// type Event struct {
|
||||
// Type string
|
||||
// Timestamp time.Time
|
||||
// Data map[string]any
|
||||
// }
|
||||
//
|
||||
// // Consumers for different aspects
|
||||
// logType := func(t string) { fmt.Printf("Type: %s\n", t) }
|
||||
// logTime := func(t time.Time) { fmt.Printf("Time: %v\n", t) }
|
||||
//
|
||||
// // Adapt them to consume events
|
||||
// logEventType := consumer.Local(func(e Event) string { return e.Type })(logType)
|
||||
// logEventTime := consumer.Local(func(e Event) time.Time { return e.Timestamp })(logTime)
|
||||
//
|
||||
// event := Event{Type: "UserLogin", Timestamp: time.Now(), Data: nil}
|
||||
// logEventType(event) // Logs: "Type: UserLogin"
|
||||
// logEventTime(event) // Logs: "Time: ..."
|
||||
//
|
||||
// Use Cases:
|
||||
// - Type adaptation: Convert between different input types
|
||||
// - Field extraction: Extract specific fields from complex structures
|
||||
// - Data transformation: Preprocess data before consumption
|
||||
// - Interface adaptation: Adapt consumers to work with different interfaces
|
||||
// - Logging pipelines: Transform data before logging
|
||||
// - Event handling: Extract relevant data from events before processing
|
||||
//
|
||||
// Relationship to Reader:
|
||||
// Consumer is the dual of Reader in category theory:
|
||||
// - Reader[R, A] = R -> A (produces output from environment)
|
||||
// - Consumer[A] = A -> () (consumes input, produces side effects)
|
||||
// - reader.Local transforms the environment before reading
|
||||
// - consumer.Local transforms the input before consuming
|
||||
// - Both are contravariant functors on their input type
|
||||
func Local[R2, R1 any](f func(R2) R1) Operator[R1, R2] {
|
||||
return func(c Consumer[R1]) Consumer[R2] {
|
||||
return func(r2 R2) {
|
||||
c(f(r2))
|
||||
}
|
||||
}
|
||||
}
|
||||
383
v2/consumer/consumer_test.go
Normal file
383
v2/consumer/consumer_test.go
Normal file
@@ -0,0 +1,383 @@
|
||||
// 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 consumer
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLocal(t *testing.T) {
|
||||
t.Run("basic type transformation", func(t *testing.T) {
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
// Transform string to int before consuming
|
||||
stringToInt := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
consumeString := Local(stringToInt)(consumeInt)
|
||||
consumeString("42")
|
||||
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
|
||||
t.Run("field extraction from struct", func(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
var capturedName string
|
||||
consumeName := func(name string) {
|
||||
capturedName = name
|
||||
}
|
||||
|
||||
extractName := func(u User) string {
|
||||
return u.Name
|
||||
}
|
||||
|
||||
consumeUser := Local(extractName)(consumeName)
|
||||
consumeUser(User{Name: "Alice", Age: 30})
|
||||
|
||||
assert.Equal(t, "Alice", capturedName)
|
||||
})
|
||||
|
||||
t.Run("simplifying complex types", func(t *testing.T) {
|
||||
type DetailedConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
Timeout time.Duration
|
||||
MaxRetry int
|
||||
}
|
||||
|
||||
type SimpleConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
var captured SimpleConfig
|
||||
consumeSimple := func(c SimpleConfig) {
|
||||
captured = c
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Host: d.Host, Port: d.Port}
|
||||
}
|
||||
|
||||
consumeDetailed := Local(simplify)(consumeSimple)
|
||||
consumeDetailed(DetailedConfig{
|
||||
Host: "localhost",
|
||||
Port: 8080,
|
||||
Timeout: time.Second,
|
||||
MaxRetry: 3,
|
||||
})
|
||||
|
||||
assert.Equal(t, SimpleConfig{Host: "localhost", Port: 8080}, captured)
|
||||
})
|
||||
|
||||
t.Run("multiple transformations", func(t *testing.T) {
|
||||
type Response struct {
|
||||
StatusCode int
|
||||
Body string
|
||||
}
|
||||
|
||||
var capturedStatus int
|
||||
consumeStatus := func(code int) {
|
||||
capturedStatus = code
|
||||
}
|
||||
|
||||
getStatus := func(r Response) int {
|
||||
return r.StatusCode
|
||||
}
|
||||
|
||||
consumeResponse := Local(getStatus)(consumeStatus)
|
||||
consumeResponse(Response{StatusCode: 200, Body: "OK"})
|
||||
|
||||
assert.Equal(t, 200, capturedStatus)
|
||||
})
|
||||
|
||||
t.Run("chaining Local transformations", func(t *testing.T) {
|
||||
type Level3 struct{ Value int }
|
||||
type Level2 struct{ L3 Level3 }
|
||||
type Level1 struct{ L2 Level2 }
|
||||
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
// Chain multiple Local transformations
|
||||
extract3 := func(l3 Level3) int { return l3.Value }
|
||||
extract2 := func(l2 Level2) Level3 { return l2.L3 }
|
||||
extract1 := func(l1 Level1) Level2 { return l1.L2 }
|
||||
|
||||
// Compose the transformations
|
||||
consumeLevel3 := Local(extract3)(consumeInt)
|
||||
consumeLevel2 := Local(extract2)(consumeLevel3)
|
||||
consumeLevel1 := Local(extract1)(consumeLevel2)
|
||||
|
||||
consumeLevel1(Level1{L2: Level2{L3: Level3{Value: 42}}})
|
||||
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
|
||||
t.Run("identity transformation", func(t *testing.T) {
|
||||
var captured string
|
||||
consumeString := func(s string) {
|
||||
captured = s
|
||||
}
|
||||
|
||||
identity := function.Identity[string]
|
||||
|
||||
consumeIdentity := Local(identity)(consumeString)
|
||||
consumeIdentity("test")
|
||||
|
||||
assert.Equal(t, "test", captured)
|
||||
})
|
||||
|
||||
t.Run("transformation with calculation", func(t *testing.T) {
|
||||
type Rectangle struct {
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
var capturedArea int
|
||||
consumeArea := func(area int) {
|
||||
capturedArea = area
|
||||
}
|
||||
|
||||
calculateArea := func(r Rectangle) int {
|
||||
return r.Width * r.Height
|
||||
}
|
||||
|
||||
consumeRectangle := Local(calculateArea)(consumeArea)
|
||||
consumeRectangle(Rectangle{Width: 5, Height: 10})
|
||||
|
||||
assert.Equal(t, 50, capturedArea)
|
||||
})
|
||||
|
||||
t.Run("multiple consumers with same transformation", func(t *testing.T) {
|
||||
type Event struct {
|
||||
Type string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
var capturedType string
|
||||
var capturedTime time.Time
|
||||
|
||||
consumeType := func(t string) {
|
||||
capturedType = t
|
||||
}
|
||||
|
||||
consumeTime := func(t time.Time) {
|
||||
capturedTime = t
|
||||
}
|
||||
|
||||
extractType := func(e Event) string { return e.Type }
|
||||
extractTime := func(e Event) time.Time { return e.Timestamp }
|
||||
|
||||
consumeEventType := Local(extractType)(consumeType)
|
||||
consumeEventTime := Local(extractTime)(consumeTime)
|
||||
|
||||
now := time.Now()
|
||||
event := Event{Type: "UserLogin", Timestamp: now}
|
||||
|
||||
consumeEventType(event)
|
||||
consumeEventTime(event)
|
||||
|
||||
assert.Equal(t, "UserLogin", capturedType)
|
||||
assert.Equal(t, now, capturedTime)
|
||||
})
|
||||
|
||||
t.Run("transformation with slice", func(t *testing.T) {
|
||||
var captured int
|
||||
consumeLength := func(n int) {
|
||||
captured = n
|
||||
}
|
||||
|
||||
getLength := func(s []string) int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
consumeSlice := Local(getLength)(consumeLength)
|
||||
consumeSlice([]string{"a", "b", "c"})
|
||||
|
||||
assert.Equal(t, 3, captured)
|
||||
})
|
||||
|
||||
t.Run("transformation with map", func(t *testing.T) {
|
||||
var captured int
|
||||
consumeCount := func(n int) {
|
||||
captured = n
|
||||
}
|
||||
|
||||
getCount := func(m map[string]int) int {
|
||||
return len(m)
|
||||
}
|
||||
|
||||
consumeMap := Local(getCount)(consumeCount)
|
||||
consumeMap(map[string]int{"a": 1, "b": 2, "c": 3})
|
||||
|
||||
assert.Equal(t, 3, captured)
|
||||
})
|
||||
|
||||
t.Run("transformation with pointer", func(t *testing.T) {
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
dereference := func(p *int) int {
|
||||
if p == nil {
|
||||
return 0
|
||||
}
|
||||
return *p
|
||||
}
|
||||
|
||||
consumePointer := Local(dereference)(consumeInt)
|
||||
|
||||
value := 42
|
||||
consumePointer(&value)
|
||||
assert.Equal(t, 42, captured)
|
||||
|
||||
consumePointer(nil)
|
||||
assert.Equal(t, 0, captured)
|
||||
})
|
||||
|
||||
t.Run("transformation with custom type", func(t *testing.T) {
|
||||
type MyType struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
var captured string
|
||||
consumeString := func(s string) {
|
||||
captured = s
|
||||
}
|
||||
|
||||
extractValue := func(m MyType) string {
|
||||
return m.Value
|
||||
}
|
||||
|
||||
consumeMyType := Local(extractValue)(consumeString)
|
||||
consumeMyType(MyType{Value: "test"})
|
||||
|
||||
assert.Equal(t, "test", captured)
|
||||
})
|
||||
|
||||
t.Run("accumulation through multiple calls", func(t *testing.T) {
|
||||
var sum int
|
||||
accumulate := func(x int) {
|
||||
sum += x
|
||||
}
|
||||
|
||||
double := func(x int) int {
|
||||
return x * 2
|
||||
}
|
||||
|
||||
accumulateDoubled := Local(double)(accumulate)
|
||||
|
||||
accumulateDoubled(1)
|
||||
accumulateDoubled(2)
|
||||
accumulateDoubled(3)
|
||||
|
||||
assert.Equal(t, 12, sum) // (1*2) + (2*2) + (3*2) = 2 + 4 + 6 = 12
|
||||
})
|
||||
|
||||
t.Run("transformation with error handling", func(t *testing.T) {
|
||||
type Result struct {
|
||||
Value int
|
||||
Error error
|
||||
}
|
||||
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
extractValue := func(r Result) int {
|
||||
if r.Error != nil {
|
||||
return -1
|
||||
}
|
||||
return r.Value
|
||||
}
|
||||
|
||||
consumeResult := Local(extractValue)(consumeInt)
|
||||
|
||||
consumeResult(Result{Value: 42, Error: nil})
|
||||
assert.Equal(t, 42, captured)
|
||||
|
||||
consumeResult(Result{Value: 100, Error: assert.AnError})
|
||||
assert.Equal(t, -1, captured)
|
||||
})
|
||||
|
||||
t.Run("transformation preserves consumer behavior", func(t *testing.T) {
|
||||
callCount := 0
|
||||
consumer := func(x int) {
|
||||
callCount++
|
||||
}
|
||||
|
||||
transform := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
transformedConsumer := Local(transform)(consumer)
|
||||
|
||||
transformedConsumer("1")
|
||||
transformedConsumer("2")
|
||||
transformedConsumer("3")
|
||||
|
||||
assert.Equal(t, 3, callCount)
|
||||
})
|
||||
|
||||
t.Run("comparison with reader.Local behavior", func(t *testing.T) {
|
||||
// This test demonstrates the dual nature of Consumer and Reader
|
||||
// Consumer: transforms input before consumption (contravariant)
|
||||
// Reader: transforms environment before reading (also contravariant on input)
|
||||
|
||||
type DetailedEnv struct {
|
||||
Value int
|
||||
Extra string
|
||||
}
|
||||
|
||||
type SimpleEnv struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
var captured int
|
||||
consumeSimple := func(e SimpleEnv) {
|
||||
captured = e.Value
|
||||
}
|
||||
|
||||
simplify := func(d DetailedEnv) SimpleEnv {
|
||||
return SimpleEnv{Value: d.Value}
|
||||
}
|
||||
|
||||
consumeDetailed := Local(simplify)(consumeSimple)
|
||||
consumeDetailed(DetailedEnv{Value: 42, Extra: "ignored"})
|
||||
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
}
|
||||
56
v2/consumer/types.go
Normal file
56
v2/consumer/types.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// 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 consumer provides types and utilities for functions that consume values without returning results.
|
||||
//
|
||||
// A Consumer represents a side-effecting operation that accepts a value but produces no output.
|
||||
// This is useful for operations like logging, printing, updating state, or any action where
|
||||
// the return value is not needed.
|
||||
package consumer
|
||||
|
||||
type (
|
||||
// Consumer represents a function that accepts a value of type A and performs a side effect.
|
||||
// It does not return any value, making it useful for operations where only the side effect matters,
|
||||
// such as logging, printing, or updating external state.
|
||||
//
|
||||
// This is a fundamental concept in functional programming for handling side effects in a
|
||||
// controlled manner. Consumers can be composed, chained, or used in higher-order functions
|
||||
// to build complex side-effecting behaviors.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of value consumed by the function
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // A simple consumer that prints values
|
||||
// var printInt Consumer[int] = func(x int) {
|
||||
// fmt.Println(x)
|
||||
// }
|
||||
// printInt(42) // Prints: 42
|
||||
//
|
||||
// // A consumer that logs messages
|
||||
// var logger Consumer[string] = func(msg string) {
|
||||
// log.Println(msg)
|
||||
// }
|
||||
// logger("Hello, World!") // Logs: Hello, World!
|
||||
//
|
||||
// // Consumers can be used in functional pipelines
|
||||
// var saveToDatabase Consumer[User] = func(user User) {
|
||||
// db.Save(user)
|
||||
// }
|
||||
Consumer[A any] = func(A)
|
||||
|
||||
Operator[A, B any] = func(Consumer[A]) Consumer[B]
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
13
v2/context/readerio/consumer.go
Normal file
13
v2/context/readerio/consumer.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package readerio
|
||||
|
||||
import "github.com/IBM/fp-go/v2/io"
|
||||
|
||||
//go:inline
|
||||
func ChainConsumer[A any](c Consumer[A]) Operator[A, struct{}] {
|
||||
return ChainIOK(io.FromConsumerK(c))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainFirstConsumer[A any](c Consumer[A]) Operator[A, A] {
|
||||
return ChainFirstIOK(io.FromConsumerK(c))
|
||||
}
|
||||
20
v2/context/readerio/flip.go
Normal file
20
v2/context/readerio/flip.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package readerio
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
RIO "github.com/IBM/fp-go/v2/readerio"
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func SequenceReader[R, A any](ma ReaderIO[Reader[R, A]]) Reader[R, ReaderIO[A]] {
|
||||
return RIO.SequenceReader(ma)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TraverseReader[R, A, B any](
|
||||
f reader.Kleisli[R, A, B],
|
||||
) func(ReaderIO[A]) Kleisli[R, B] {
|
||||
return RIO.TraverseReader[context.Context](f)
|
||||
}
|
||||
29
v2/context/readerio/logging.go
Normal file
29
v2/context/readerio/logging.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package readerio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/IBM/fp-go/v2/logging"
|
||||
)
|
||||
|
||||
func SLogWithCallback[A any](
|
||||
logLevel slog.Level,
|
||||
cb func(context.Context) *slog.Logger,
|
||||
message string) Kleisli[A, A] {
|
||||
return func(a A) ReaderIO[A] {
|
||||
return func(ctx context.Context) IO[A] {
|
||||
// logger
|
||||
logger := cb(ctx)
|
||||
return func() A {
|
||||
logger.LogAttrs(ctx, logLevel, message, slog.Any("value", a))
|
||||
return a
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func SLog[A any](message string) Kleisli[A, A] {
|
||||
return SLogWithCallback[A](slog.LevelInfo, logging.GetLoggerFromContext, message)
|
||||
}
|
||||
755
v2/context/readerio/reader.go
Normal file
755
v2/context/readerio/reader.go
Normal file
@@ -0,0 +1,755 @@
|
||||
// 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 readerio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
RIO "github.com/IBM/fp-go/v2/readerio"
|
||||
)
|
||||
|
||||
const (
|
||||
// useParallel is the feature flag to control if we use the parallel or the sequential implementation of ap
|
||||
useParallel = true
|
||||
)
|
||||
|
||||
// MonadMap transforms the success value of a [ReaderIO] using the provided function.
|
||||
// If the computation fails, the error is propagated unchanged.
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: The ReaderIO to transform
|
||||
// - f: The transformation function
|
||||
//
|
||||
// Returns a new ReaderIO with the transformed value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadMap[A, B any](fa ReaderIO[A], f func(A) B) ReaderIO[B] {
|
||||
return RIO.MonadMap(fa, f)
|
||||
}
|
||||
|
||||
// Map transforms the success value of a [ReaderIO] using the provided function.
|
||||
// This is the curried version of [MonadMap], useful for composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: The transformation function
|
||||
//
|
||||
// Returns a function that transforms a ReaderIO.
|
||||
//
|
||||
//go:inline
|
||||
func Map[A, B any](f func(A) B) Operator[A, B] {
|
||||
return RIO.Map[context.Context](f)
|
||||
}
|
||||
|
||||
// MonadMapTo replaces the success value of a [ReaderIO] with a constant value.
|
||||
// If the computation fails, the error is propagated unchanged.
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: The ReaderIO to transform
|
||||
// - b: The constant value to use
|
||||
//
|
||||
// Returns a new ReaderIO with the constant value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadMapTo[A, B any](fa ReaderIO[A], b B) ReaderIO[B] {
|
||||
return RIO.MonadMapTo(fa, b)
|
||||
}
|
||||
|
||||
// MapTo replaces the success value of a [ReaderIO] with a constant value.
|
||||
// This is the curried version of [MonadMapTo].
|
||||
//
|
||||
// Parameters:
|
||||
// - b: The constant value to use
|
||||
//
|
||||
// Returns a function that transforms a ReaderIO.
|
||||
//
|
||||
//go:inline
|
||||
func MapTo[A, B any](b B) Operator[A, B] {
|
||||
return RIO.MapTo[context.Context, A](b)
|
||||
}
|
||||
|
||||
// MonadChain sequences two [ReaderIO] computations, where the second depends on the result of the first.
|
||||
// If the first computation fails, the second is not executed.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The first ReaderIO
|
||||
// - f: Function that produces the second ReaderIO based on the first's result
|
||||
//
|
||||
// Returns a new ReaderIO representing the sequenced computation.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChain[A, B any](ma ReaderIO[A], f Kleisli[A, B]) ReaderIO[B] {
|
||||
return RIO.MonadChain(ma, f)
|
||||
}
|
||||
|
||||
// Chain sequences two [ReaderIO] computations, where the second depends on the result of the first.
|
||||
// This is the curried version of [MonadChain], useful for composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces the second ReaderIO based on the first's result
|
||||
//
|
||||
// Returns a function that sequences ReaderIO computations.
|
||||
//
|
||||
//go:inline
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||
return RIO.Chain(f)
|
||||
}
|
||||
|
||||
// MonadChainFirst sequences two [ReaderIO] computations but returns the result of the first.
|
||||
// The second computation is executed for its side effects only.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The first ReaderIO
|
||||
// - f: Function that produces the second ReaderIO
|
||||
//
|
||||
// Returns a ReaderIO with the result of the first computation.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirst[A, B any](ma ReaderIO[A], f Kleisli[A, B]) ReaderIO[A] {
|
||||
return RIO.MonadChainFirst(ma, f)
|
||||
}
|
||||
|
||||
// MonadTap executes a side-effect computation but returns the original value.
|
||||
// This is an alias for [MonadChainFirst] and is useful for operations like logging
|
||||
// or validation that should not affect the main computation flow.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The ReaderIO to tap
|
||||
// - f: Function that produces a side-effect ReaderIO
|
||||
//
|
||||
// Returns a ReaderIO with the original value after executing the side effect.
|
||||
//
|
||||
//go:inline
|
||||
func MonadTap[A, B any](ma ReaderIO[A], f Kleisli[A, B]) ReaderIO[A] {
|
||||
return RIO.MonadTap(ma, f)
|
||||
}
|
||||
|
||||
// ChainFirst sequences two [ReaderIO] computations but returns the result of the first.
|
||||
// This is the curried version of [MonadChainFirst].
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces the second ReaderIO
|
||||
//
|
||||
// Returns a function that sequences ReaderIO computations.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
||||
return RIO.ChainFirst(f)
|
||||
}
|
||||
|
||||
// Tap executes a side-effect computation but returns the original value.
|
||||
// This is the curried version of [MonadTap], an alias for [ChainFirst].
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces a side-effect ReaderIO
|
||||
//
|
||||
// Returns a function that taps ReaderIO computations.
|
||||
//
|
||||
//go:inline
|
||||
func Tap[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
||||
return RIO.Tap(f)
|
||||
}
|
||||
|
||||
// Of creates a [ReaderIO] that always succeeds with the given value.
|
||||
// This is the same as [Right] and represents the monadic return operation.
|
||||
//
|
||||
// Parameters:
|
||||
// - a: The value to wrap
|
||||
//
|
||||
// Returns a ReaderIO that always succeeds with the given value.
|
||||
//
|
||||
//go:inline
|
||||
func Of[A any](a A) ReaderIO[A] {
|
||||
return RIO.Of[context.Context](a)
|
||||
}
|
||||
|
||||
// MonadApPar implements parallel applicative application for [ReaderIO].
|
||||
// It executes the function and value computations in parallel where possible,
|
||||
// potentially improving performance for independent operations.
|
||||
//
|
||||
// Parameters:
|
||||
// - fab: ReaderIO containing a function
|
||||
// - fa: ReaderIO containing a value
|
||||
//
|
||||
// Returns a ReaderIO with the function applied to the value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadApPar[B, A any](fab ReaderIO[func(A) B], fa ReaderIO[A]) ReaderIO[B] {
|
||||
return RIO.MonadApPar(fab, fa)
|
||||
}
|
||||
|
||||
// MonadAp implements applicative application for [ReaderIO].
|
||||
// By default, it uses parallel execution ([MonadApPar]) but can be configured to use
|
||||
// sequential execution ([MonadApSeq]) via the useParallel constant.
|
||||
//
|
||||
// Parameters:
|
||||
// - fab: ReaderIO containing a function
|
||||
// - fa: ReaderIO containing a value
|
||||
//
|
||||
// Returns a ReaderIO with the function applied to the value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadAp[B, A any](fab ReaderIO[func(A) B], fa ReaderIO[A]) ReaderIO[B] {
|
||||
// dispatch to the configured version
|
||||
if useParallel {
|
||||
return MonadApPar(fab, fa)
|
||||
}
|
||||
return MonadApSeq(fab, fa)
|
||||
}
|
||||
|
||||
// MonadApSeq implements sequential applicative application for [ReaderIO].
|
||||
// It executes the function computation first, then the value computation.
|
||||
//
|
||||
// Parameters:
|
||||
// - fab: ReaderIO containing a function
|
||||
// - fa: ReaderIO containing a value
|
||||
//
|
||||
// Returns a ReaderIO with the function applied to the value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadApSeq[B, A any](fab ReaderIO[func(A) B], fa ReaderIO[A]) ReaderIO[B] {
|
||||
return RIO.MonadApSeq(fab, fa)
|
||||
}
|
||||
|
||||
// Ap applies a function wrapped in a [ReaderIO] to a value wrapped in a ReaderIO.
|
||||
// This is the curried version of [MonadAp], using the default execution mode.
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: ReaderIO containing a value
|
||||
//
|
||||
// Returns a function that applies a ReaderIO function to the value.
|
||||
//
|
||||
//go:inline
|
||||
func Ap[B, A any](fa ReaderIO[A]) Operator[func(A) B, B] {
|
||||
return RIO.Ap[B](fa)
|
||||
}
|
||||
|
||||
// ApSeq applies a function wrapped in a [ReaderIO] to a value sequentially.
|
||||
// This is the curried version of [MonadApSeq].
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: ReaderIO containing a value
|
||||
//
|
||||
// Returns a function that applies a ReaderIO function to the value sequentially.
|
||||
//
|
||||
//go:inline
|
||||
func ApSeq[B, A any](fa ReaderIO[A]) Operator[func(A) B, B] {
|
||||
return function.Bind2nd(MonadApSeq[B, A], fa)
|
||||
}
|
||||
|
||||
// ApPar applies a function wrapped in a [ReaderIO] to a value in parallel.
|
||||
// This is the curried version of [MonadApPar].
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: ReaderIO containing a value
|
||||
//
|
||||
// Returns a function that applies a ReaderIO function to the value in parallel.
|
||||
//
|
||||
//go:inline
|
||||
func ApPar[B, A any](fa ReaderIO[A]) Operator[func(A) B, B] {
|
||||
return function.Bind2nd(MonadApPar[B, A], fa)
|
||||
}
|
||||
|
||||
// Ask returns a [ReaderIO] that provides access to the context.
|
||||
// This is useful for accessing the [context.Context] within a computation.
|
||||
//
|
||||
// Returns a ReaderIO that produces the context.
|
||||
//
|
||||
//go:inline
|
||||
func Ask() ReaderIO[context.Context] {
|
||||
return RIO.Ask[context.Context]()
|
||||
}
|
||||
|
||||
// FromIO converts an [IO] into a [ReaderIO].
|
||||
// The IO computation always succeeds, so it's wrapped in Right.
|
||||
//
|
||||
// Parameters:
|
||||
// - t: The IO to convert
|
||||
//
|
||||
// Returns a ReaderIO that executes the IO and wraps the result in Right.
|
||||
//
|
||||
//go:inline
|
||||
func FromIO[A any](t IO[A]) ReaderIO[A] {
|
||||
return RIO.FromIO[context.Context](t)
|
||||
}
|
||||
|
||||
// FromReader converts a [Reader] into a [ReaderIO].
|
||||
// The Reader computation is lifted into the IO context, allowing it to be
|
||||
// composed with other ReaderIO operations.
|
||||
//
|
||||
// Parameters:
|
||||
// - t: The Reader to convert
|
||||
//
|
||||
// Returns a ReaderIO that executes the Reader and wraps the result in IO.
|
||||
//
|
||||
//go:inline
|
||||
func FromReader[A any](t Reader[context.Context, A]) ReaderIO[A] {
|
||||
return RIO.FromReader(t)
|
||||
}
|
||||
|
||||
// FromLazy converts a [Lazy] computation into a [ReaderIO].
|
||||
// The Lazy computation always succeeds, so it's wrapped in Right.
|
||||
// This is an alias for [FromIO] since Lazy and IO have the same structure.
|
||||
//
|
||||
// Parameters:
|
||||
// - t: The Lazy computation to convert
|
||||
//
|
||||
// Returns a ReaderIO that executes the Lazy computation and wraps the result in Right.
|
||||
//
|
||||
//go:inline
|
||||
func FromLazy[A any](t Lazy[A]) ReaderIO[A] {
|
||||
return RIO.FromIO[context.Context](t)
|
||||
}
|
||||
|
||||
// MonadChainIOK chains a function that returns an [IO] into a [ReaderIO] computation.
|
||||
// The IO computation always succeeds, so it's wrapped in Right.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The ReaderIO to chain from
|
||||
// - f: Function that produces an IO
|
||||
//
|
||||
// Returns a new ReaderIO with the chained IO computation.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainIOK[A, B any](ma ReaderIO[A], f func(A) IO[B]) ReaderIO[B] {
|
||||
return RIO.MonadChainIOK(ma, f)
|
||||
}
|
||||
|
||||
// ChainIOK chains a function that returns an [IO] into a [ReaderIO] computation.
|
||||
// This is the curried version of [MonadChainIOK].
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces an IO
|
||||
//
|
||||
// Returns a function that chains the IO-returning function.
|
||||
//
|
||||
//go:inline
|
||||
func ChainIOK[A, B any](f func(A) IO[B]) Operator[A, B] {
|
||||
return RIO.ChainIOK[context.Context](f)
|
||||
}
|
||||
|
||||
// MonadChainFirstIOK chains a function that returns an [IO] but keeps the original value.
|
||||
// The IO computation is executed for its side effects only.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The ReaderIO to chain from
|
||||
// - f: Function that produces an IO
|
||||
//
|
||||
// Returns a ReaderIO with the original value after executing the IO.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstIOK[A, B any](ma ReaderIO[A], f func(A) IO[B]) ReaderIO[A] {
|
||||
return RIO.MonadChainFirstIOK(ma, f)
|
||||
}
|
||||
|
||||
// MonadTapIOK chains a function that returns an [IO] but keeps the original value.
|
||||
// This is an alias for [MonadChainFirstIOK] and is useful for side effects like logging.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The ReaderIO to tap
|
||||
// - f: Function that produces an IO for side effects
|
||||
//
|
||||
// Returns a ReaderIO with the original value after executing the IO.
|
||||
//
|
||||
//go:inline
|
||||
func MonadTapIOK[A, B any](ma ReaderIO[A], f func(A) IO[B]) ReaderIO[A] {
|
||||
return RIO.MonadTapIOK(ma, f)
|
||||
}
|
||||
|
||||
// ChainFirstIOK chains a function that returns an [IO] but keeps the original value.
|
||||
// This is the curried version of [MonadChainFirstIOK].
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces an IO
|
||||
//
|
||||
// Returns a function that chains the IO-returning function.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
|
||||
return RIO.ChainFirstIOK[context.Context](f)
|
||||
}
|
||||
|
||||
// TapIOK chains a function that returns an [IO] but keeps the original value.
|
||||
// This is the curried version of [MonadTapIOK], an alias for [ChainFirstIOK].
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces an IO for side effects
|
||||
//
|
||||
// Returns a function that taps with IO-returning functions.
|
||||
//
|
||||
//go:inline
|
||||
func TapIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
|
||||
return RIO.TapIOK[context.Context](f)
|
||||
}
|
||||
|
||||
// Defer creates a [ReaderIO] by lazily generating a new computation each time it's executed.
|
||||
// This is useful for creating computations that should be re-evaluated on each execution.
|
||||
//
|
||||
// Parameters:
|
||||
// - gen: Lazy generator function that produces a ReaderIO
|
||||
//
|
||||
// Returns a ReaderIO that generates a fresh computation on each execution.
|
||||
//
|
||||
//go:inline
|
||||
func Defer[A any](gen Lazy[ReaderIO[A]]) ReaderIO[A] {
|
||||
return RIO.Defer(gen)
|
||||
}
|
||||
|
||||
// Memoize computes the value of the provided [ReaderIO] monad lazily but exactly once.
|
||||
// The context used to compute the value is the context of the first call, so do not use this
|
||||
// method if the value has a functional dependency on the content of the context.
|
||||
//
|
||||
// Parameters:
|
||||
// - rdr: The ReaderIO to memoize
|
||||
//
|
||||
// Returns a ReaderIO that caches its result after the first execution.
|
||||
//
|
||||
//go:inline
|
||||
func Memoize[A any](rdr ReaderIO[A]) ReaderIO[A] {
|
||||
return RIO.Memoize(rdr)
|
||||
}
|
||||
|
||||
// Flatten converts a nested [ReaderIO] into a flat [ReaderIO].
|
||||
// This is equivalent to [MonadChain] with the identity function.
|
||||
//
|
||||
// Parameters:
|
||||
// - rdr: The nested ReaderIO to flatten
|
||||
//
|
||||
// Returns a flattened ReaderIO.
|
||||
//
|
||||
//go:inline
|
||||
func Flatten[A any](rdr ReaderIO[ReaderIO[A]]) ReaderIO[A] {
|
||||
return RIO.Flatten(rdr)
|
||||
}
|
||||
|
||||
// MonadFlap applies a value to a function wrapped in a [ReaderIO].
|
||||
// This is the reverse of [MonadAp], useful in certain composition scenarios.
|
||||
//
|
||||
// Parameters:
|
||||
// - fab: ReaderIO containing a function
|
||||
// - a: The value to apply to the function
|
||||
//
|
||||
// Returns a ReaderIO with the function applied to the value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadFlap[B, A any](fab ReaderIO[func(A) B], a A) ReaderIO[B] {
|
||||
return RIO.MonadFlap(fab, a)
|
||||
}
|
||||
|
||||
// Flap applies a value to a function wrapped in a [ReaderIO].
|
||||
// This is the curried version of [MonadFlap].
|
||||
//
|
||||
// Parameters:
|
||||
// - a: The value to apply to the function
|
||||
//
|
||||
// Returns a function that applies the value to a ReaderIO function.
|
||||
//
|
||||
//go:inline
|
||||
func Flap[B, A any](a A) Operator[func(A) B, B] {
|
||||
return RIO.Flap[context.Context, B](a)
|
||||
}
|
||||
|
||||
// MonadChainReaderK chains a [ReaderIO] with a function that returns a [Reader].
|
||||
// The Reader is lifted into the ReaderIO context, allowing composition of
|
||||
// Reader and ReaderIO operations.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The ReaderIO to chain from
|
||||
// - f: Function that produces a Reader
|
||||
//
|
||||
// Returns a new ReaderIO with the chained Reader computation.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainReaderK[A, B any](ma ReaderIO[A], f reader.Kleisli[context.Context, A, B]) ReaderIO[B] {
|
||||
return RIO.MonadChainReaderK(ma, f)
|
||||
}
|
||||
|
||||
// ChainReaderK chains a [ReaderIO] with a function that returns a [Reader].
|
||||
// This is the curried version of [MonadChainReaderK].
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces a Reader
|
||||
//
|
||||
// Returns a function that chains Reader-returning functions.
|
||||
//
|
||||
//go:inline
|
||||
func ChainReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, B] {
|
||||
return RIO.ChainReaderK(f)
|
||||
}
|
||||
|
||||
// MonadChainFirstReaderK chains a function that returns a [Reader] but keeps the original value.
|
||||
// The Reader computation is executed for its side effects only.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The ReaderIO to chain from
|
||||
// - f: Function that produces a Reader
|
||||
//
|
||||
// Returns a ReaderIO with the original value after executing the Reader.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstReaderK[A, B any](ma ReaderIO[A], f reader.Kleisli[context.Context, A, B]) ReaderIO[A] {
|
||||
return RIO.MonadChainFirstReaderK(ma, f)
|
||||
}
|
||||
|
||||
// MonadTapReaderK chains a function that returns a [Reader] but keeps the original value.
|
||||
// This is an alias for [MonadChainFirstReaderK] and is useful for side effects.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The ReaderIO to tap
|
||||
// - f: Function that produces a Reader for side effects
|
||||
//
|
||||
// Returns a ReaderIO with the original value after executing the Reader.
|
||||
//
|
||||
//go:inline
|
||||
func MonadTapReaderK[A, B any](ma ReaderIO[A], f reader.Kleisli[context.Context, A, B]) ReaderIO[A] {
|
||||
return RIO.MonadTapReaderK(ma, f)
|
||||
}
|
||||
|
||||
// ChainFirstReaderK chains a function that returns a [Reader] but keeps the original value.
|
||||
// This is the curried version of [MonadChainFirstReaderK].
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces a Reader
|
||||
//
|
||||
// Returns a function that chains Reader-returning functions while preserving the original value.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, A] {
|
||||
return RIO.ChainFirstReaderK(f)
|
||||
}
|
||||
|
||||
// TapReaderK chains a function that returns a [Reader] but keeps the original value.
|
||||
// This is the curried version of [MonadTapReaderK], an alias for [ChainFirstReaderK].
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces a Reader for side effects
|
||||
//
|
||||
// Returns a function that taps with Reader-returning functions.
|
||||
//
|
||||
//go:inline
|
||||
func TapReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, A] {
|
||||
return RIO.TapReaderK(f)
|
||||
}
|
||||
|
||||
// Read executes a [ReaderIO] with a given context, returning the resulting [IO].
|
||||
// This is useful for providing the context dependency and obtaining an IO action
|
||||
// that can be executed later.
|
||||
//
|
||||
// Parameters:
|
||||
// - r: The context to provide to the ReaderIO
|
||||
//
|
||||
// Returns a function that converts a ReaderIO into an IO by applying the context.
|
||||
//
|
||||
//go:inline
|
||||
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)
|
||||
})
|
||||
}
|
||||
502
v2/context/readerio/reader_test.go
Normal file
502
v2/context/readerio/reader_test.go
Normal file
@@ -0,0 +1,502 @@
|
||||
// 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 readerio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
G "github.com/IBM/fp-go/v2/io"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMonadMap(t *testing.T) {
|
||||
rio := Of(5)
|
||||
doubled := MonadMap(rio, N.Mul(2))
|
||||
|
||||
result := doubled(context.Background())()
|
||||
assert.Equal(t, 10, result)
|
||||
}
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Of(1),
|
||||
Map(utils.Double),
|
||||
)
|
||||
|
||||
assert.Equal(t, 2, g(context.Background())())
|
||||
}
|
||||
|
||||
func TestMonadMapTo(t *testing.T) {
|
||||
rio := Of(42)
|
||||
replaced := MonadMapTo(rio, "constant")
|
||||
|
||||
result := replaced(context.Background())()
|
||||
assert.Equal(t, "constant", result)
|
||||
}
|
||||
|
||||
func TestMapTo(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of(42),
|
||||
MapTo[int]("constant"),
|
||||
)
|
||||
|
||||
assert.Equal(t, "constant", result(context.Background())())
|
||||
}
|
||||
|
||||
func TestMonadChain(t *testing.T) {
|
||||
rio1 := Of(5)
|
||||
result := MonadChain(rio1, func(n int) ReaderIO[int] {
|
||||
return Of(n * 3)
|
||||
})
|
||||
|
||||
assert.Equal(t, 15, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestChain(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of(5),
|
||||
Chain(func(n int) ReaderIO[int] {
|
||||
return Of(n * 3)
|
||||
}),
|
||||
)
|
||||
|
||||
assert.Equal(t, 15, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestMonadChainFirst(t *testing.T) {
|
||||
sideEffect := 0
|
||||
rio := Of(42)
|
||||
result := MonadChainFirst(rio, func(n int) ReaderIO[string] {
|
||||
sideEffect = n
|
||||
return Of("side effect")
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestChainFirst(t *testing.T) {
|
||||
sideEffect := 0
|
||||
result := F.Pipe1(
|
||||
Of(42),
|
||||
ChainFirst(func(n int) ReaderIO[string] {
|
||||
sideEffect = n
|
||||
return Of("side effect")
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestMonadTap(t *testing.T) {
|
||||
sideEffect := 0
|
||||
rio := Of(42)
|
||||
result := MonadTap(rio, func(n int) ReaderIO[func()] {
|
||||
sideEffect = n
|
||||
return Of(func() {})
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestTap(t *testing.T) {
|
||||
sideEffect := 0
|
||||
result := F.Pipe1(
|
||||
Of(42),
|
||||
Tap(func(n int) ReaderIO[func()] {
|
||||
sideEffect = n
|
||||
return Of(func() {})
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestOf(t *testing.T) {
|
||||
rio := Of(100)
|
||||
result := rio(context.Background())()
|
||||
|
||||
assert.Equal(t, 100, result)
|
||||
}
|
||||
|
||||
func TestMonadAp(t *testing.T) {
|
||||
fabIO := Of(N.Mul(2))
|
||||
faIO := Of(5)
|
||||
result := MonadAp(fabIO, faIO)
|
||||
|
||||
assert.Equal(t, 10, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestAp(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Of(utils.Double),
|
||||
Ap[int](Of(1)),
|
||||
)
|
||||
|
||||
assert.Equal(t, 2, g(context.Background())())
|
||||
}
|
||||
|
||||
func TestMonadApSeq(t *testing.T) {
|
||||
fabIO := Of(N.Add(10))
|
||||
faIO := Of(5)
|
||||
result := MonadApSeq(fabIO, faIO)
|
||||
|
||||
assert.Equal(t, 15, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestApSeq(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Of(N.Add(10)),
|
||||
ApSeq[int](Of(5)),
|
||||
)
|
||||
|
||||
assert.Equal(t, 15, g(context.Background())())
|
||||
}
|
||||
|
||||
func TestMonadApPar(t *testing.T) {
|
||||
fabIO := Of(N.Add(10))
|
||||
faIO := Of(5)
|
||||
result := MonadApPar(fabIO, faIO)
|
||||
|
||||
assert.Equal(t, 15, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestApPar(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Of(N.Add(10)),
|
||||
ApPar[int](Of(5)),
|
||||
)
|
||||
|
||||
assert.Equal(t, 15, g(context.Background())())
|
||||
}
|
||||
|
||||
func TestAsk(t *testing.T) {
|
||||
rio := Ask()
|
||||
ctx := context.WithValue(context.Background(), "key", "value")
|
||||
result := rio(ctx)()
|
||||
|
||||
assert.Equal(t, ctx, result)
|
||||
}
|
||||
|
||||
func TestFromIO(t *testing.T) {
|
||||
ioAction := G.Of(42)
|
||||
rio := FromIO(ioAction)
|
||||
|
||||
result := rio(context.Background())()
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
func TestFromReader(t *testing.T) {
|
||||
rdr := func(ctx context.Context) int {
|
||||
return 42
|
||||
}
|
||||
|
||||
rio := FromReader(rdr)
|
||||
result := rio(context.Background())()
|
||||
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
func TestFromLazy(t *testing.T) {
|
||||
lazy := func() int { return 42 }
|
||||
rio := FromLazy(lazy)
|
||||
|
||||
result := rio(context.Background())()
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
func TestMonadChainIOK(t *testing.T) {
|
||||
rio := Of(5)
|
||||
result := MonadChainIOK(rio, func(n int) G.IO[int] {
|
||||
return G.Of(n * 4)
|
||||
})
|
||||
|
||||
assert.Equal(t, 20, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestChainIOK(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of(5),
|
||||
ChainIOK(func(n int) G.IO[int] {
|
||||
return G.Of(n * 4)
|
||||
}),
|
||||
)
|
||||
|
||||
assert.Equal(t, 20, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestMonadChainFirstIOK(t *testing.T) {
|
||||
sideEffect := 0
|
||||
rio := Of(42)
|
||||
result := MonadChainFirstIOK(rio, func(n int) G.IO[string] {
|
||||
sideEffect = n
|
||||
return G.Of("side effect")
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestChainFirstIOK(t *testing.T) {
|
||||
sideEffect := 0
|
||||
result := F.Pipe1(
|
||||
Of(42),
|
||||
ChainFirstIOK(func(n int) G.IO[string] {
|
||||
sideEffect = n
|
||||
return G.Of("side effect")
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestMonadTapIOK(t *testing.T) {
|
||||
sideEffect := 0
|
||||
rio := Of(42)
|
||||
result := MonadTapIOK(rio, func(n int) G.IO[func()] {
|
||||
sideEffect = n
|
||||
return G.Of(func() {})
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestTapIOK(t *testing.T) {
|
||||
sideEffect := 0
|
||||
result := F.Pipe1(
|
||||
Of(42),
|
||||
TapIOK(func(n int) G.IO[func()] {
|
||||
sideEffect = n
|
||||
return G.Of(func() {})
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestDefer(t *testing.T) {
|
||||
counter := 0
|
||||
rio := Defer(func() ReaderIO[int] {
|
||||
counter++
|
||||
return Of(counter)
|
||||
})
|
||||
|
||||
result1 := rio(context.Background())()
|
||||
result2 := rio(context.Background())()
|
||||
|
||||
assert.Equal(t, 1, result1)
|
||||
assert.Equal(t, 2, result2)
|
||||
}
|
||||
|
||||
func TestMemoize(t *testing.T) {
|
||||
counter := 0
|
||||
rio := Of(0)
|
||||
memoized := Memoize(MonadMap(rio, func(int) int {
|
||||
counter++
|
||||
return counter
|
||||
}))
|
||||
|
||||
result1 := memoized(context.Background())()
|
||||
result2 := memoized(context.Background())()
|
||||
|
||||
assert.Equal(t, 1, result1)
|
||||
assert.Equal(t, 1, result2) // Same value, memoized
|
||||
}
|
||||
|
||||
func TestFlatten(t *testing.T) {
|
||||
nested := Of(Of(42))
|
||||
flattened := Flatten(nested)
|
||||
|
||||
result := flattened(context.Background())()
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
func TestMonadFlap(t *testing.T) {
|
||||
fabIO := Of(N.Mul(3))
|
||||
result := MonadFlap(fabIO, 7)
|
||||
|
||||
assert.Equal(t, 21, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestFlap(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of(N.Mul(3)),
|
||||
Flap[int](7),
|
||||
)
|
||||
|
||||
assert.Equal(t, 21, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestMonadChainReaderK(t *testing.T) {
|
||||
rio := Of(5)
|
||||
result := MonadChainReaderK(rio, func(n int) reader.Reader[context.Context, int] {
|
||||
return func(ctx context.Context) int { return n * 2 }
|
||||
})
|
||||
|
||||
assert.Equal(t, 10, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestChainReaderK(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of(5),
|
||||
ChainReaderK(func(n int) reader.Reader[context.Context, int] {
|
||||
return func(ctx context.Context) int { return n * 2 }
|
||||
}),
|
||||
)
|
||||
|
||||
assert.Equal(t, 10, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestMonadChainFirstReaderK(t *testing.T) {
|
||||
sideEffect := 0
|
||||
rio := Of(42)
|
||||
result := MonadChainFirstReaderK(rio, func(n int) reader.Reader[context.Context, string] {
|
||||
return func(ctx context.Context) string {
|
||||
sideEffect = n
|
||||
return "side effect"
|
||||
}
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestChainFirstReaderK(t *testing.T) {
|
||||
sideEffect := 0
|
||||
result := F.Pipe1(
|
||||
Of(42),
|
||||
ChainFirstReaderK(func(n int) reader.Reader[context.Context, string] {
|
||||
return func(ctx context.Context) string {
|
||||
sideEffect = n
|
||||
return "side effect"
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestMonadTapReaderK(t *testing.T) {
|
||||
sideEffect := 0
|
||||
rio := Of(42)
|
||||
result := MonadTapReaderK(rio, func(n int) reader.Reader[context.Context, func()] {
|
||||
return func(ctx context.Context) func() {
|
||||
sideEffect = n
|
||||
return func() {}
|
||||
}
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestTapReaderK(t *testing.T) {
|
||||
sideEffect := 0
|
||||
result := F.Pipe1(
|
||||
Of(42),
|
||||
TapReaderK(func(n int) reader.Reader[context.Context, func()] {
|
||||
return func(ctx context.Context) func() {
|
||||
sideEffect = n
|
||||
return func() {}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
rio := Of(42)
|
||||
ctx := context.Background()
|
||||
ioAction := Read[int](ctx)(rio)
|
||||
result := ioAction()
|
||||
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
func TestComplexPipeline(t *testing.T) {
|
||||
// Test a complex pipeline combining multiple operations
|
||||
result := F.Pipe3(
|
||||
Ask(),
|
||||
Map(func(ctx context.Context) int { return 5 }),
|
||||
Chain(func(n int) ReaderIO[int] {
|
||||
return Of(n * 2)
|
||||
}),
|
||||
Map(N.Add(10)),
|
||||
)
|
||||
|
||||
assert.Equal(t, 20, result(context.Background())()) // (5 * 2) + 10 = 20
|
||||
}
|
||||
|
||||
func TestFromIOWithChain(t *testing.T) {
|
||||
ioAction := G.Of(10)
|
||||
|
||||
result := F.Pipe1(
|
||||
FromIO(ioAction),
|
||||
Chain(func(n int) ReaderIO[int] {
|
||||
return Of(n + 5)
|
||||
}),
|
||||
)
|
||||
|
||||
assert.Equal(t, 15, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestTapWithLogging(t *testing.T) {
|
||||
// Simulate logging scenario
|
||||
logged := []int{}
|
||||
|
||||
result := F.Pipe3(
|
||||
Of(42),
|
||||
Tap(func(n int) ReaderIO[func()] {
|
||||
logged = append(logged, n)
|
||||
return Of(func() {})
|
||||
}),
|
||||
Map(N.Mul(2)),
|
||||
Tap(func(n int) ReaderIO[func()] {
|
||||
logged = append(logged, n)
|
||||
return Of(func() {})
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
assert.Equal(t, 84, value)
|
||||
assert.Equal(t, []int{42, 84}, logged)
|
||||
}
|
||||
25
v2/context/readerio/rec.go
Normal file
25
v2/context/readerio/rec.go
Normal file
@@ -0,0 +1,25 @@
|
||||
// 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 readerio
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func TailRec[A, B any](f Kleisli[A, Either[A, B]]) Kleisli[A, B] {
|
||||
return readerio.TailRec(f)
|
||||
}
|
||||
75
v2/context/readerio/type.go
Normal file
75
v2/context/readerio/type.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 readerio
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/consumer"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
)
|
||||
|
||||
type (
|
||||
// Lazy represents a deferred computation that produces a value of type A when executed.
|
||||
// The computation is not executed until explicitly invoked.
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
|
||||
// IO represents a side-effectful computation that produces a value of type A.
|
||||
// The computation is deferred and only executed when invoked.
|
||||
//
|
||||
// IO[A] is equivalent to func() A
|
||||
IO[A any] = io.IO[A]
|
||||
|
||||
// Reader represents a computation that depends on a context of type R.
|
||||
// This is used for dependency injection and accessing shared context.
|
||||
//
|
||||
// Reader[R, A] is equivalent to func(R) A
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
|
||||
// ReaderIO represents a context-dependent computation that performs side effects.
|
||||
// This is specialized to use [context.Context] as the context type.
|
||||
//
|
||||
// ReaderIO[A] is equivalent to func(context.Context) func() A
|
||||
ReaderIO[A any] = readerio.ReaderIO[context.Context, A]
|
||||
|
||||
// Kleisli represents a Kleisli arrow for the ReaderIO monad.
|
||||
// It is a function that takes a value of type A and returns a ReaderIO computation
|
||||
// that produces a value of type B.
|
||||
//
|
||||
// Kleisli arrows are used for composing monadic computations and are fundamental
|
||||
// to functional programming patterns involving effects and context.
|
||||
//
|
||||
// Kleisli[A, B] is equivalent to func(A) func(context.Context) func() B
|
||||
Kleisli[A, B any] = reader.Reader[A, ReaderIO[B]]
|
||||
|
||||
// Operator represents a transformation from one ReaderIO computation to another.
|
||||
// It takes a ReaderIO[A] and returns a ReaderIO[B], allowing for the composition
|
||||
// of context-dependent, side-effectful computations.
|
||||
//
|
||||
// Operators are useful for building pipelines of ReaderIO computations where
|
||||
// each step can depend on the previous computation's result.
|
||||
//
|
||||
// Operator[A, B] is equivalent to func(ReaderIO[A]) func(context.Context) func() B
|
||||
Operator[A, B any] = Kleisli[ReaderIO[A], B]
|
||||
|
||||
Consumer[A any] = consumer.Consumer[A]
|
||||
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
)
|
||||
504
v2/context/readerioresult/FLIP_POINTFREE.md
Normal file
504
v2/context/readerioresult/FLIP_POINTFREE.md
Normal file
@@ -0,0 +1,504 @@
|
||||
# Sequence Functions and Point-Free Style Programming
|
||||
|
||||
This document explains how the `Sequence*` functions in the `context/readerioresult` package enable point-free style programming and improve code composition.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [What is Point-Free Style?](#what-is-point-free-style)
|
||||
2. [The Problem: Nested Function Application](#the-problem-nested-function-application)
|
||||
3. [The Solution: Sequence Functions](#the-solution-sequence-functions)
|
||||
4. [How Sequence Enables Point-Free Style](#how-sequence-enables-point-free-style)
|
||||
5. [Practical Benefits](#practical-benefits)
|
||||
6. [Examples](#examples)
|
||||
7. [Comparison: With and Without Sequence](#comparison-with-and-without-sequence)
|
||||
|
||||
## What is Point-Free Style?
|
||||
|
||||
Point-free style (also called tacit programming) is a programming paradigm where function definitions don't explicitly mention their arguments. Instead, functions are composed using combinators and higher-order functions.
|
||||
|
||||
**Traditional style (with points):**
|
||||
```go
|
||||
func double(x int) int {
|
||||
return x * 2
|
||||
}
|
||||
```
|
||||
|
||||
**Point-free style (without points):**
|
||||
```go
|
||||
var double = F.Flow2(
|
||||
N.Mul(2),
|
||||
identity,
|
||||
)
|
||||
```
|
||||
|
||||
The key benefit is that point-free style emphasizes **what** the function does (its transformation) rather than **how** it manipulates data.
|
||||
|
||||
## The Problem: Nested Function Application
|
||||
|
||||
In functional programming with monadic types like `ReaderIOResult`, we often have nested structures where we need to apply parameters in a specific order. Consider:
|
||||
|
||||
```go
|
||||
type ReaderIOResult[A any] = func(context.Context) func() Either[error, A]
|
||||
type Reader[R, A any] = func(R) A
|
||||
|
||||
// A computation that produces a Reader
|
||||
type Computation = ReaderIOResult[Reader[Config, int]]
|
||||
// Expands to: func(context.Context) func() Either[error, func(Config) int]
|
||||
```
|
||||
|
||||
To use this, we must apply parameters in this order:
|
||||
1. First, provide `context.Context`
|
||||
2. Then, execute the IO effect (call the function)
|
||||
3. Then, unwrap the `Either` to get the `Reader`
|
||||
4. Finally, provide the `Config`
|
||||
|
||||
This creates several problems:
|
||||
|
||||
### Problem 1: Awkward Parameter Order
|
||||
|
||||
```go
|
||||
computation := getComputation()
|
||||
ctx := context.Background()
|
||||
cfg := Config{Value: 42}
|
||||
|
||||
// Must apply in this specific order
|
||||
result := computation(ctx)() // Get Either[error, Reader[Config, int]]
|
||||
if reader, err := either.Unwrap(result); err == nil {
|
||||
value := reader(cfg) // Finally apply Config
|
||||
// use value
|
||||
}
|
||||
```
|
||||
|
||||
The `Config` parameter, which is often known early and stable, must be provided last. This prevents partial application and reuse.
|
||||
|
||||
### Problem 2: Cannot Partially Apply Dependencies
|
||||
|
||||
```go
|
||||
// Want to do this: create a reusable computation with Config baked in
|
||||
// But can't because Config comes last!
|
||||
withConfig := computation(cfg) // ❌ Doesn't work - cfg comes last, not first
|
||||
```
|
||||
|
||||
### Problem 3: Breaks Point-Free Composition
|
||||
|
||||
```go
|
||||
// Want to compose like this:
|
||||
var pipeline = F.Flow3(
|
||||
getComputation,
|
||||
applyConfig(cfg), // ❌ Can't do this - Config comes last
|
||||
processResult,
|
||||
)
|
||||
```
|
||||
|
||||
## The Solution: Sequence Functions
|
||||
|
||||
The `Sequence*` functions solve this by "flipping" or "sequencing" the nested structure, changing the order in which parameters are applied.
|
||||
|
||||
### SequenceReader
|
||||
|
||||
```go
|
||||
func SequenceReader[R, A any](
|
||||
ma ReaderIOResult[Reader[R, A]]
|
||||
) reader.Kleisli[context.Context, R, IOResult[A]]
|
||||
```
|
||||
|
||||
**Type transformation:**
|
||||
```
|
||||
From: func(context.Context) func() Either[error, func(R) A]
|
||||
To: func(R) func(context.Context) func() Either[error, A]
|
||||
```
|
||||
|
||||
Now `R` (the Reader's environment) comes **first**, before `context.Context`!
|
||||
|
||||
### SequenceReaderIO
|
||||
|
||||
```go
|
||||
func SequenceReaderIO[R, A any](
|
||||
ma ReaderIOResult[ReaderIO[R, A]]
|
||||
) reader.Kleisli[context.Context, R, IOResult[A]]
|
||||
```
|
||||
|
||||
**Type transformation:**
|
||||
```
|
||||
From: func(context.Context) func() Either[error, func(R) func() A]
|
||||
To: func(R) func(context.Context) func() Either[error, A]
|
||||
```
|
||||
|
||||
### SequenceReaderResult
|
||||
|
||||
```go
|
||||
func SequenceReaderResult[R, A any](
|
||||
ma ReaderIOResult[ReaderResult[R, A]]
|
||||
) reader.Kleisli[context.Context, R, IOResult[A]]
|
||||
```
|
||||
|
||||
**Type transformation:**
|
||||
```
|
||||
From: func(context.Context) func() Either[error, func(R) Either[error, A]]
|
||||
To: func(R) func(context.Context) func() Either[error, A]
|
||||
```
|
||||
|
||||
## How Sequence Enables Point-Free Style
|
||||
|
||||
### 1. Partial Application
|
||||
|
||||
By moving the environment parameter first, we can partially apply it:
|
||||
|
||||
```go
|
||||
type Config struct { Multiplier int }
|
||||
|
||||
computation := getComputation() // ReaderIOResult[Reader[Config, int]]
|
||||
sequenced := SequenceReader[Config, int](computation)
|
||||
|
||||
// Partially apply Config
|
||||
cfg := Config{Multiplier: 5}
|
||||
withConfig := sequenced(cfg) // ✅ Now we have ReaderIOResult[int]
|
||||
|
||||
// Reuse with different contexts
|
||||
result1 := withConfig(ctx1)()
|
||||
result2 := withConfig(ctx2)()
|
||||
```
|
||||
|
||||
### 2. Dependency Injection
|
||||
|
||||
Inject dependencies early in the pipeline:
|
||||
|
||||
```go
|
||||
type Database struct { ConnectionString string }
|
||||
|
||||
makeQuery := func(ctx context.Context) func() Either[error, func(Database) string] {
|
||||
// ... implementation
|
||||
}
|
||||
|
||||
// Sequence to enable DI
|
||||
queryWithDB := SequenceReader[Database, string](makeQuery)
|
||||
|
||||
// Inject database
|
||||
db := Database{ConnectionString: "localhost:5432"}
|
||||
query := queryWithDB(db) // ✅ Database injected
|
||||
|
||||
// Use query with any context
|
||||
result := query(context.Background())()
|
||||
```
|
||||
|
||||
### 3. Point-Free Composition
|
||||
|
||||
Build pipelines without mentioning intermediate values:
|
||||
|
||||
```go
|
||||
var pipeline = F.Flow3(
|
||||
getComputation, // ReaderIOResult[Reader[Config, int]]
|
||||
SequenceReader[Config, int], // func(Config) ReaderIOResult[int]
|
||||
applyConfig(cfg), // ReaderIOResult[int]
|
||||
)
|
||||
|
||||
// Or with partial application:
|
||||
var withConfig = F.Pipe1(
|
||||
getComputation(),
|
||||
SequenceReader[Config, int],
|
||||
)
|
||||
|
||||
result := withConfig(cfg)(ctx)()
|
||||
```
|
||||
|
||||
### 4. Reusable Computations
|
||||
|
||||
Create specialized versions of generic computations:
|
||||
|
||||
```go
|
||||
// Generic computation
|
||||
makeServiceInfo := func(ctx context.Context) func() Either[error, func(ServiceConfig) string] {
|
||||
// ... implementation
|
||||
}
|
||||
|
||||
sequenced := SequenceReader[ServiceConfig, string](makeServiceInfo)
|
||||
|
||||
// Create specialized versions
|
||||
authService := sequenced(ServiceConfig{Name: "Auth", Version: "1.0"})
|
||||
userService := sequenced(ServiceConfig{Name: "User", Version: "2.0"})
|
||||
|
||||
// Reuse across contexts
|
||||
authInfo := authService(ctx)()
|
||||
userInfo := userService(ctx)()
|
||||
```
|
||||
|
||||
## Practical Benefits
|
||||
|
||||
### 1. **Improved Testability**
|
||||
|
||||
Inject test dependencies easily:
|
||||
|
||||
```go
|
||||
// Production
|
||||
prodDB := Database{ConnectionString: "prod:5432"}
|
||||
prodQuery := queryWithDB(prodDB)
|
||||
|
||||
// Testing
|
||||
testDB := Database{ConnectionString: "test:5432"}
|
||||
testQuery := queryWithDB(testDB)
|
||||
|
||||
// Same computation, different dependencies
|
||||
```
|
||||
|
||||
### 2. **Better Separation of Concerns**
|
||||
|
||||
Separate configuration from execution:
|
||||
|
||||
```go
|
||||
// Configuration phase (pure, no effects)
|
||||
cfg := loadConfig()
|
||||
computation := sequenced(cfg)
|
||||
|
||||
// Execution phase (with effects)
|
||||
result := computation(ctx)()
|
||||
```
|
||||
|
||||
### 3. **Enhanced Composability**
|
||||
|
||||
Build complex pipelines from simple pieces:
|
||||
|
||||
```go
|
||||
var processUser = F.Flow4(
|
||||
loadUserConfig, // ReaderIOResult[Reader[Database, User]]
|
||||
SequenceReader, // func(Database) ReaderIOResult[User]
|
||||
applyDatabase(db), // ReaderIOResult[User]
|
||||
Chain(validateUser), // ReaderIOResult[ValidatedUser]
|
||||
)
|
||||
```
|
||||
|
||||
### 4. **Reduced Boilerplate**
|
||||
|
||||
No need to manually thread parameters:
|
||||
|
||||
```go
|
||||
// Without Sequence - manual threading
|
||||
func processWithConfig(cfg Config) ReaderIOResult[Result] {
|
||||
return func(ctx context.Context) func() Either[error, Result] {
|
||||
return func() Either[error, Result] {
|
||||
comp := getComputation()(ctx)()
|
||||
if reader, err := either.Unwrap(comp); err == nil {
|
||||
value := reader(cfg)
|
||||
// ... more processing
|
||||
}
|
||||
// ... error handling
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// With Sequence - point-free
|
||||
var processWithConfig = F.Flow2(
|
||||
getComputation,
|
||||
SequenceReader[Config, Result],
|
||||
)
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Database Query with Configuration
|
||||
|
||||
```go
|
||||
type QueryConfig struct {
|
||||
Timeout time.Duration
|
||||
MaxRows int
|
||||
}
|
||||
|
||||
type Database struct {
|
||||
ConnectionString string
|
||||
}
|
||||
|
||||
// Without Sequence
|
||||
func executeQueryOld(cfg QueryConfig, db Database) ReaderIOResult[[]Row] {
|
||||
return func(ctx context.Context) func() Either[error, []Row] {
|
||||
return func() Either[error, []Row] {
|
||||
// Must manually handle all parameters
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// With Sequence
|
||||
func makeQuery(ctx context.Context) func() Either[error, func(Database) []Row] {
|
||||
return func() Either[error, func(Database) []Row] {
|
||||
return Right[error](func(db Database) []Row {
|
||||
// Implementation
|
||||
return []Row{}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var executeQuery = F.Flow2(
|
||||
makeQuery,
|
||||
SequenceReader[Database, []Row],
|
||||
)
|
||||
|
||||
// Usage
|
||||
db := Database{ConnectionString: "localhost:5432"}
|
||||
query := executeQuery(db)
|
||||
result := query(ctx)()
|
||||
```
|
||||
|
||||
### Example 2: Multi-Service Architecture
|
||||
|
||||
```go
|
||||
type ServiceRegistry struct {
|
||||
AuthService AuthService
|
||||
UserService UserService
|
||||
EmailService EmailService
|
||||
}
|
||||
|
||||
// Create computations that depend on services
|
||||
makeAuthCheck := func(ctx context.Context) func() Either[error, func(ServiceRegistry) bool] {
|
||||
// ... implementation
|
||||
}
|
||||
|
||||
makeSendEmail := func(ctx context.Context) func() Either[error, func(ServiceRegistry) error] {
|
||||
// ... implementation
|
||||
}
|
||||
|
||||
// Sequence them
|
||||
authCheck := SequenceReader[ServiceRegistry, bool](makeAuthCheck)
|
||||
sendEmail := SequenceReader[ServiceRegistry, error](makeSendEmail)
|
||||
|
||||
// Inject services once
|
||||
registry := ServiceRegistry{ /* ... */ }
|
||||
checkAuth := authCheck(registry)
|
||||
sendMail := sendEmail(registry)
|
||||
|
||||
// Use with different contexts
|
||||
if isAuth, _ := either.Unwrap(checkAuth(ctx1)()); isAuth {
|
||||
sendMail(ctx2)()
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: Configuration-Driven Pipeline
|
||||
|
||||
```go
|
||||
type PipelineConfig struct {
|
||||
Stage1Config Stage1Config
|
||||
Stage2Config Stage2Config
|
||||
Stage3Config Stage3Config
|
||||
}
|
||||
|
||||
// Define stages
|
||||
stage1 := SequenceReader[Stage1Config, IntermediateResult1](makeStage1)
|
||||
stage2 := SequenceReader[Stage2Config, IntermediateResult2](makeStage2)
|
||||
stage3 := SequenceReader[Stage3Config, FinalResult](makeStage3)
|
||||
|
||||
// Build pipeline with configuration
|
||||
func buildPipeline(cfg PipelineConfig) ReaderIOResult[FinalResult] {
|
||||
return F.Pipe3(
|
||||
stage1(cfg.Stage1Config),
|
||||
Chain(func(r1 IntermediateResult1) ReaderIOResult[IntermediateResult2] {
|
||||
return stage2(cfg.Stage2Config)
|
||||
}),
|
||||
Chain(func(r2 IntermediateResult2) ReaderIOResult[FinalResult] {
|
||||
return stage3(cfg.Stage3Config)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// Execute pipeline
|
||||
cfg := loadPipelineConfig()
|
||||
pipeline := buildPipeline(cfg)
|
||||
result := pipeline(ctx)()
|
||||
```
|
||||
|
||||
## Comparison: With and Without Sequence
|
||||
|
||||
### Without Sequence (Imperative Style)
|
||||
|
||||
```go
|
||||
func processUser(userID string) ReaderIOResult[ProcessedUser] {
|
||||
return func(ctx context.Context) func() Either[error, ProcessedUser] {
|
||||
return func() Either[error, ProcessedUser] {
|
||||
// Get database
|
||||
dbComp := getDatabase()(ctx)()
|
||||
if dbReader, err := either.Unwrap(dbComp); err != nil {
|
||||
return Left[ProcessedUser](err)
|
||||
}
|
||||
db := dbReader(dbConfig)
|
||||
|
||||
// Get user
|
||||
userComp := getUser(userID)(ctx)()
|
||||
if userReader, err := either.Unwrap(userComp); err != nil {
|
||||
return Left[ProcessedUser](err)
|
||||
}
|
||||
user := userReader(db)
|
||||
|
||||
// Process user
|
||||
processComp := processUserData(user)(ctx)()
|
||||
if processReader, err := either.Unwrap(processComp); err != nil {
|
||||
return Left[ProcessedUser](err)
|
||||
}
|
||||
result := processReader(processingConfig)
|
||||
|
||||
return Right[error](result)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### With Sequence (Point-Free Style)
|
||||
|
||||
```go
|
||||
var processUser = func(userID string) ReaderIOResult[ProcessedUser] {
|
||||
return F.Pipe3(
|
||||
getDatabase,
|
||||
SequenceReader[DatabaseConfig, Database],
|
||||
applyConfig(dbConfig),
|
||||
Chain(func(db Database) ReaderIOResult[User] {
|
||||
return F.Pipe2(
|
||||
getUser(userID),
|
||||
SequenceReader[Database, User],
|
||||
applyDB(db),
|
||||
)
|
||||
}),
|
||||
Chain(func(user User) ReaderIOResult[ProcessedUser] {
|
||||
return F.Pipe2(
|
||||
processUserData(user),
|
||||
SequenceReader[ProcessingConfig, ProcessedUser],
|
||||
applyConfig(processingConfig),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Key Takeaways
|
||||
|
||||
1. **Sequence functions flip parameter order** to enable partial application
|
||||
2. **Dependencies come first**, making them easy to inject and test
|
||||
3. **Point-free style** becomes natural and readable
|
||||
4. **Composition** is enhanced through proper parameter ordering
|
||||
5. **Reusability** increases as computations can be specialized early
|
||||
6. **Testability** improves through easy dependency injection
|
||||
7. **Separation of concerns** is clearer (configuration vs. execution)
|
||||
|
||||
## When to Use Sequence
|
||||
|
||||
Use `Sequence*` functions when:
|
||||
|
||||
- ✅ You want to partially apply environment/configuration parameters
|
||||
- ✅ You're building reusable computations with injected dependencies
|
||||
- ✅ You need to test with different dependency implementations
|
||||
- ✅ You're composing complex pipelines in point-free style
|
||||
- ✅ You want to separate configuration from execution
|
||||
- ✅ You're working with nested Reader-like structures
|
||||
|
||||
Don't use `Sequence*` when:
|
||||
|
||||
- ❌ The original parameter order is already optimal
|
||||
- ❌ You're not doing any composition or partial application
|
||||
- ❌ The added abstraction doesn't provide value
|
||||
- ❌ The code is simpler without it
|
||||
|
||||
## Conclusion
|
||||
|
||||
The `Sequence*` functions are powerful tools for enabling point-free style programming in Go. By flipping the parameter order of nested monadic structures, they make it easy to:
|
||||
|
||||
- Partially apply dependencies
|
||||
- Build composable pipelines
|
||||
- Improve testability
|
||||
- Write more declarative code
|
||||
|
||||
While they add a layer of abstraction, the benefits in terms of code reusability, testability, and composability make them invaluable for functional programming in Go.
|
||||
@@ -18,6 +18,7 @@ package readerioresult
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/readerio"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/apply"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
@@ -25,7 +26,6 @@ import (
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
@@ -96,7 +96,7 @@ func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f Kleisli[S1, T],
|
||||
) Operator[S1, S2] {
|
||||
return RIOR.Bind(setter, f)
|
||||
return RIOR.Bind(setter, F.Flow2(f, WithContext))
|
||||
}
|
||||
|
||||
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
@@ -256,7 +256,7 @@ func BindL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Kleisli[T, T],
|
||||
) Operator[S, S] {
|
||||
return RIOR.BindL(lens, f)
|
||||
return RIOR.BindL(lens, F.Flow2(f, WithContext))
|
||||
}
|
||||
|
||||
// LetL is a variant of Let that uses a lens to focus on a specific part of the context.
|
||||
@@ -290,7 +290,7 @@ func BindL[S, T any](
|
||||
//go:inline
|
||||
func LetL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f func(T) T,
|
||||
f Endomorphism[T],
|
||||
) Operator[S, S] {
|
||||
return RIOR.LetL[context.Context](lens, f)
|
||||
}
|
||||
@@ -398,7 +398,7 @@ func BindReaderK[S1, S2, T any](
|
||||
//go:inline
|
||||
func BindReaderIOK[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f readerio.Kleisli[context.Context, S1, T],
|
||||
f readerio.Kleisli[S1, T],
|
||||
) Operator[S1, S2] {
|
||||
return Bind(setter, F.Flow2(f, FromReaderIO[T]))
|
||||
}
|
||||
@@ -507,7 +507,7 @@ func BindReaderKL[S, T any](
|
||||
//go:inline
|
||||
func BindReaderIOKL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f readerio.Kleisli[context.Context, T, T],
|
||||
f readerio.Kleisli[T, T],
|
||||
) Operator[S, S] {
|
||||
return BindL(lens, F.Flow2(f, FromReaderIO[T]))
|
||||
}
|
||||
|
||||
@@ -203,9 +203,7 @@ func TestApS_EmptyState(t *testing.T) {
|
||||
result := res(t.Context())()
|
||||
assert.True(t, E.IsRight(result))
|
||||
emptyOpt := E.ToOption(result)
|
||||
assert.True(t, O.IsSome(emptyOpt))
|
||||
empty, _ := O.Unwrap(emptyOpt)
|
||||
assert.Equal(t, Empty{}, empty)
|
||||
assert.Equal(t, O.Of(Empty{}), emptyOpt)
|
||||
}
|
||||
|
||||
func TestApS_ChainedWithBind(t *testing.T) {
|
||||
|
||||
@@ -16,11 +16,14 @@
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||
)
|
||||
|
||||
// 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.
|
||||
//
|
||||
//go:inline
|
||||
func Bracket[
|
||||
A, B, ANY any](
|
||||
|
||||
@@ -28,5 +31,5 @@ func Bracket[
|
||||
use Kleisli[A, B],
|
||||
release func(A, Either[B]) ReaderIOResult[ANY],
|
||||
) ReaderIOResult[B] {
|
||||
return RIOR.Bracket(acquire, use, release)
|
||||
return RIOR.Bracket(acquire, F.Flow2(use, WithContext), release)
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@ import (
|
||||
// Returns a ReaderIOResult that checks for cancellation before executing.
|
||||
func WithContext[A any](ma ReaderIOResult[A]) ReaderIOResult[A] {
|
||||
return func(ctx context.Context) IOEither[A] {
|
||||
if err := context.Cause(ctx); err != nil {
|
||||
return ioeither.Left[A](err)
|
||||
if ctx.Err() != nil {
|
||||
return ioeither.Left[A](context.Cause(ctx))
|
||||
}
|
||||
return CIOE.WithContext(ctx, ma(ctx))
|
||||
}
|
||||
|
||||
13
v2/context/readerioresult/consumer.go
Normal file
13
v2/context/readerioresult/consumer.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package readerioresult
|
||||
|
||||
import "github.com/IBM/fp-go/v2/io"
|
||||
|
||||
//go:inline
|
||||
func ChainConsumer[A any](c Consumer[A]) Operator[A, struct{}] {
|
||||
return ChainIOK(io.FromConsumerK(c))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainFirstConsumer[A any](c Consumer[A]) Operator[A, A] {
|
||||
return ChainFirstIOK(io.FromConsumerK(c))
|
||||
}
|
||||
295
v2/context/readerioresult/flip.go
Normal file
295
v2/context/readerioresult/flip.go
Normal file
@@ -0,0 +1,295 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
RIO "github.com/IBM/fp-go/v2/readerio"
|
||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||
RR "github.com/IBM/fp-go/v2/readerresult"
|
||||
)
|
||||
|
||||
// SequenceReader transforms a ReaderIOResult containing a Reader into a function that
|
||||
// takes the Reader's environment first, then returns a ReaderIOResult.
|
||||
//
|
||||
// This function "flips" or "sequences" the nested structure, changing the order in which
|
||||
// parameters are applied. It's particularly useful for point-free style programming where
|
||||
// you want to partially apply the inner Reader's environment before dealing with the
|
||||
// outer context.
|
||||
//
|
||||
// Type transformation:
|
||||
//
|
||||
// From: ReaderIOResult[Reader[R, A]]
|
||||
// = func(context.Context) func() Either[error, func(R) A]
|
||||
//
|
||||
// To: func(context.Context) func(R) IOResult[A]
|
||||
// = func(context.Context) func(R) func() Either[error, A]
|
||||
//
|
||||
// This allows you to:
|
||||
// 1. Provide the context.Context first
|
||||
// 2. Then provide the Reader's environment R
|
||||
// 3. Finally execute the IO effect to get Either[error, A]
|
||||
//
|
||||
// Point-free style benefits:
|
||||
// - Enables partial application of the Reader environment
|
||||
// - Facilitates composition of Reader-based computations
|
||||
// - Allows building reusable computation pipelines
|
||||
// - Supports dependency injection patterns where R represents dependencies
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Timeout int
|
||||
// }
|
||||
//
|
||||
// // A computation that produces a Reader based on context
|
||||
// func getMultiplier(ctx context.Context) func() Either[error, func(Config) int] {
|
||||
// return func() Either[error, func(Config) int] {
|
||||
// return Right[error](func(cfg Config) int {
|
||||
// return cfg.Timeout * 2
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Sequence it to apply Config first
|
||||
// sequenced := SequenceReader[Config, int](getMultiplier)
|
||||
//
|
||||
// // Now we can partially apply the Config
|
||||
// cfg := Config{Timeout: 30}
|
||||
// ctx := context.Background()
|
||||
// result := sequenced(ctx)(cfg)() // Returns Right(60)
|
||||
//
|
||||
// This is especially useful in point-free style when building computation pipelines:
|
||||
//
|
||||
// var pipeline = F.Flow3(
|
||||
// loadConfig, // ReaderIOResult[Reader[Database, Config]]
|
||||
// SequenceReader, // func(context.Context) func(Database) IOResult[Config]
|
||||
// applyToDatabase(db), // IOResult[Config]
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func SequenceReader[R, A any](ma ReaderIOResult[Reader[R, A]]) reader.Kleisli[context.Context, R, IOResult[A]] {
|
||||
return RIOR.SequenceReader(ma)
|
||||
}
|
||||
|
||||
// SequenceReaderIO transforms a ReaderIOResult containing a ReaderIO into a function that
|
||||
// takes the ReaderIO's environment first, then returns a ReaderIOResult.
|
||||
//
|
||||
// This is similar to SequenceReader but works with ReaderIO, which represents a computation
|
||||
// that depends on an environment R and performs IO effects.
|
||||
//
|
||||
// Type transformation:
|
||||
//
|
||||
// From: ReaderIOResult[ReaderIO[R, A]]
|
||||
// = func(context.Context) func() Either[error, func(R) func() A]
|
||||
//
|
||||
// To: func(context.Context) func(R) IOResult[A]
|
||||
// = func(context.Context) func(R) func() Either[error, A]
|
||||
//
|
||||
// The key difference from SequenceReader is that the inner computation (ReaderIO) already
|
||||
// performs IO effects, so the sequencing combines these effects properly.
|
||||
//
|
||||
// Point-free style benefits:
|
||||
// - Enables composition of ReaderIO-based computations
|
||||
// - Allows partial application of environment before IO execution
|
||||
// - Facilitates building effect pipelines with dependency injection
|
||||
// - Supports layered architecture where R represents service dependencies
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Database struct {
|
||||
// ConnectionString string
|
||||
// }
|
||||
//
|
||||
// // A computation that produces a ReaderIO based on context
|
||||
// func getQuery(ctx context.Context) func() Either[error, func(Database) func() string] {
|
||||
// return func() Either[error, func(Database) func() string] {
|
||||
// return Right[error](func(db Database) func() string {
|
||||
// return func() string {
|
||||
// // Perform actual IO here
|
||||
// return "Query result from " + db.ConnectionString
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Sequence it to apply Database first
|
||||
// sequenced := SequenceReaderIO[Database, string](getQuery)
|
||||
//
|
||||
// // Partially apply the Database
|
||||
// db := Database{ConnectionString: "localhost:5432"}
|
||||
// ctx := context.Background()
|
||||
// result := sequenced(ctx)(db)() // Executes IO and returns Right("Query result...")
|
||||
//
|
||||
// In point-free style, this enables clean composition:
|
||||
//
|
||||
// var executeQuery = F.Flow3(
|
||||
// prepareQuery, // ReaderIOResult[ReaderIO[Database, QueryResult]]
|
||||
// SequenceReaderIO, // func(context.Context) func(Database) IOResult[QueryResult]
|
||||
// withDatabase(db), // IOResult[QueryResult]
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func SequenceReaderIO[R, A any](ma ReaderIOResult[RIO.ReaderIO[R, A]]) reader.Kleisli[context.Context, R, IOResult[A]] {
|
||||
return RIOR.SequenceReaderIO(ma)
|
||||
}
|
||||
|
||||
// SequenceReaderResult transforms a ReaderIOResult containing a ReaderResult into a function
|
||||
// that takes the ReaderResult's environment first, then returns a ReaderIOResult.
|
||||
//
|
||||
// This is similar to SequenceReader but works with ReaderResult, which represents a computation
|
||||
// that depends on an environment R and can fail with an error.
|
||||
//
|
||||
// Type transformation:
|
||||
//
|
||||
// From: ReaderIOResult[ReaderResult[R, A]]
|
||||
// = func(context.Context) func() Either[error, func(R) Either[error, A]]
|
||||
//
|
||||
// To: func(context.Context) func(R) IOResult[A]
|
||||
// = func(context.Context) func(R) func() Either[error, A]
|
||||
//
|
||||
// The sequencing properly combines the error handling from both the outer ReaderIOResult
|
||||
// and the inner ReaderResult, ensuring that errors from either level are propagated correctly.
|
||||
//
|
||||
// Point-free style benefits:
|
||||
// - Enables composition of error-handling computations with dependency injection
|
||||
// - Allows partial application of dependencies before error handling
|
||||
// - Facilitates building validation pipelines with environment dependencies
|
||||
// - Supports service-oriented architectures with proper error propagation
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// MaxRetries int
|
||||
// }
|
||||
//
|
||||
// // A computation that produces a ReaderResult based on context
|
||||
// func validateRetries(ctx context.Context) func() Either[error, func(Config) Either[error, int]] {
|
||||
// return func() Either[error, func(Config) Either[error, int]] {
|
||||
// return Right[error](func(cfg Config) Either[error, int] {
|
||||
// if cfg.MaxRetries < 0 {
|
||||
// return Left[int](errors.New("negative retries"))
|
||||
// }
|
||||
// return Right[error](cfg.MaxRetries)
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Sequence it to apply Config first
|
||||
// sequenced := SequenceReaderResult[Config, int](validateRetries)
|
||||
//
|
||||
// // Partially apply the Config
|
||||
// cfg := Config{MaxRetries: 3}
|
||||
// ctx := context.Background()
|
||||
// result := sequenced(ctx)(cfg)() // Returns Right(3)
|
||||
//
|
||||
// // With invalid config
|
||||
// badCfg := Config{MaxRetries: -1}
|
||||
// badResult := sequenced(ctx)(badCfg)() // Returns Left(error("negative retries"))
|
||||
//
|
||||
// In point-free style, this enables validation pipelines:
|
||||
//
|
||||
// var validateAndProcess = F.Flow4(
|
||||
// loadConfig, // ReaderIOResult[ReaderResult[Config, Settings]]
|
||||
// SequenceReaderResult, // func(context.Context) func(Config) IOResult[Settings]
|
||||
// applyConfig(cfg), // IOResult[Settings]
|
||||
// Chain(processSettings), // IOResult[Result]
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func SequenceReaderResult[R, A any](ma ReaderIOResult[RR.ReaderResult[R, A]]) reader.Kleisli[context.Context, R, IOResult[A]] {
|
||||
return RIOR.SequenceReaderEither(ma)
|
||||
}
|
||||
|
||||
// TraverseReader transforms a ReaderIOResult 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 ReaderIOResult. The result allows you to provide the Reader's environment (R)
|
||||
// first, which then produces a ReaderIOResult that depends on the context.
|
||||
//
|
||||
// Type transformation:
|
||||
//
|
||||
// From: ReaderIOResult[A]
|
||||
// = func(context.Context) func() Either[error, A]
|
||||
//
|
||||
// With: reader.Kleisli[R, A, B]
|
||||
// = func(A) func(R) B
|
||||
//
|
||||
// To: func(ReaderIOResult[A]) func(R) ReaderIOResult[B]
|
||||
// = func(ReaderIOResult[A]) func(R) func(context.Context) func() Either[error, B]
|
||||
//
|
||||
// This enables:
|
||||
// 1. Transforming values within a ReaderIOResult using environment-dependent logic
|
||||
// 2. Introducing new environment dependencies into existing computations
|
||||
// 3. Building composable pipelines where transformations depend on configuration or dependencies
|
||||
// 4. Point-free style composition with Reader-based transformations
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The environment type that the Reader depends on
|
||||
// - A: The input value type
|
||||
// - B: The output value type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Reader-based Kleisli arrow that transforms A to B using environment R
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderIOResult[A] and returns a Kleisli[R, B],
|
||||
// which is func(R) ReaderIOResult[B]
|
||||
//
|
||||
// The function preserves error handling and IO effects while adding the Reader environment dependency.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Multiplier int
|
||||
// }
|
||||
//
|
||||
// // A Reader-based transformation that depends on Config
|
||||
// multiply := func(x int) func(Config) int {
|
||||
// return func(cfg Config) int {
|
||||
// return x * cfg.Multiplier
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Original computation that produces an int
|
||||
// computation := Right[int](10)
|
||||
//
|
||||
// // Apply TraverseReader to introduce Config dependency
|
||||
// traversed := TraverseReader[Config, int, int](multiply)
|
||||
// result := traversed(computation)
|
||||
//
|
||||
// // Now we can provide the Config to get the final result
|
||||
// cfg := Config{Multiplier: 5}
|
||||
// ctx := context.Background()
|
||||
// finalResult := result(cfg)(ctx)() // Returns Right(50)
|
||||
//
|
||||
// In point-free style, this enables clean composition:
|
||||
//
|
||||
// var pipeline = F.Flow3(
|
||||
// loadValue, // ReaderIOResult[int]
|
||||
// TraverseReader(multiplyByConfig), // func(Config) ReaderIOResult[int]
|
||||
// applyConfig(cfg), // ReaderIOResult[int]
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func TraverseReader[R, A, B any](
|
||||
f reader.Kleisli[R, A, B],
|
||||
) func(ReaderIOResult[A]) Kleisli[R, B] {
|
||||
return RIOR.TraverseReader[context.Context](f)
|
||||
}
|
||||
333
v2/context/readerioresult/flip_example_test.go
Normal file
333
v2/context/readerioresult/flip_example_test.go
Normal file
@@ -0,0 +1,333 @@
|
||||
// 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_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
// Example_sequenceReader_basicUsage demonstrates the basic usage of SequenceReader
|
||||
// to flip the parameter order, enabling point-free style programming.
|
||||
func Example_sequenceReader_basicUsage() {
|
||||
type Config struct {
|
||||
Multiplier int
|
||||
}
|
||||
|
||||
// A computation that produces a Reader based on context
|
||||
getComputation := func(ctx context.Context) func() either.Either[error, func(Config) int] {
|
||||
return func() either.Either[error, func(Config) int] {
|
||||
// This could check context for cancellation, deadlines, etc.
|
||||
return either.Right[error](func(cfg Config) int {
|
||||
return cfg.Multiplier * 10
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sequence it to flip the parameter order
|
||||
// Now Config comes first, then context
|
||||
sequenced := RIOE.SequenceReader(getComputation)
|
||||
|
||||
// Partially apply the Config - this is the key benefit for point-free style
|
||||
cfg := Config{Multiplier: 5}
|
||||
withConfig := sequenced(cfg)
|
||||
|
||||
// Now we have a ReaderIOResult[int] that can be used with any context
|
||||
ctx := context.Background()
|
||||
result := withConfig(ctx)()
|
||||
|
||||
if value, err := either.Unwrap(result); err == nil {
|
||||
fmt.Println(value)
|
||||
}
|
||||
// Output: 50
|
||||
}
|
||||
|
||||
// Example_sequenceReader_dependencyInjection demonstrates how SequenceReader
|
||||
// enables clean dependency injection patterns in point-free style.
|
||||
func Example_sequenceReader_dependencyInjection() {
|
||||
// Define our dependencies
|
||||
type Database struct {
|
||||
ConnectionString string
|
||||
}
|
||||
|
||||
type UserService struct {
|
||||
db Database
|
||||
}
|
||||
|
||||
// A function that creates a computation requiring a Database
|
||||
makeQuery := func(ctx context.Context) func() either.Either[error, func(Database) string] {
|
||||
return func() either.Either[error, func(Database) string] {
|
||||
return either.Right[error](func(db Database) string {
|
||||
return fmt.Sprintf("Querying %s", db.ConnectionString)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sequence to enable dependency injection
|
||||
queryWithDB := RIOE.SequenceReader(makeQuery)
|
||||
|
||||
// Inject the database dependency
|
||||
db := Database{ConnectionString: "localhost:5432"}
|
||||
query := queryWithDB(db)
|
||||
|
||||
// Execute with context
|
||||
ctx := context.Background()
|
||||
result := query(ctx)()
|
||||
|
||||
if value, err := either.Unwrap(result); err == nil {
|
||||
fmt.Println(value)
|
||||
}
|
||||
// Output: Querying localhost:5432
|
||||
}
|
||||
|
||||
// Example_sequenceReader_pointFreeComposition demonstrates how SequenceReader
|
||||
// enables point-free style composition of computations.
|
||||
func Example_sequenceReader_pointFreeComposition() {
|
||||
type Config struct {
|
||||
BaseValue int
|
||||
}
|
||||
|
||||
// Step 1: Create a computation that produces a Reader
|
||||
step1 := func(ctx context.Context) func() either.Either[error, func(Config) int] {
|
||||
return func() either.Either[error, func(Config) int] {
|
||||
return either.Right[error](func(cfg Config) int {
|
||||
return cfg.BaseValue * 2
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Sequence it to enable partial application
|
||||
sequenced := RIOE.SequenceReader(step1)
|
||||
|
||||
// Step 3: Build a pipeline using point-free style
|
||||
// Partially apply the config
|
||||
cfg := Config{BaseValue: 10}
|
||||
|
||||
// Create a reusable computation with the config baked in
|
||||
computation := F.Pipe1(
|
||||
sequenced(cfg),
|
||||
RIOE.Map(func(x int) int { return x + 5 }),
|
||||
)
|
||||
|
||||
// Execute the pipeline
|
||||
ctx := context.Background()
|
||||
result := computation(ctx)()
|
||||
|
||||
if value, err := either.Unwrap(result); err == nil {
|
||||
fmt.Println(value)
|
||||
}
|
||||
// Output: 25
|
||||
}
|
||||
|
||||
// Example_sequenceReader_multipleEnvironments demonstrates using SequenceReader
|
||||
// to work with multiple environment types in a clean, composable way.
|
||||
func Example_sequenceReader_multipleEnvironments() {
|
||||
type DatabaseConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
type APIConfig struct {
|
||||
Endpoint string
|
||||
APIKey string
|
||||
}
|
||||
|
||||
// Function that needs DatabaseConfig
|
||||
getDatabaseURL := func(ctx context.Context) func() either.Either[error, func(DatabaseConfig) string] {
|
||||
return func() either.Either[error, func(DatabaseConfig) string] {
|
||||
return either.Right[error](func(cfg DatabaseConfig) string {
|
||||
return fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Function that needs APIConfig
|
||||
getAPIURL := func(ctx context.Context) func() either.Either[error, func(APIConfig) string] {
|
||||
return func() either.Either[error, func(APIConfig) string] {
|
||||
return either.Right[error](func(cfg APIConfig) string {
|
||||
return cfg.Endpoint
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sequence both to enable partial application
|
||||
withDBConfig := RIOE.SequenceReader(getDatabaseURL)
|
||||
withAPIConfig := RIOE.SequenceReader(getAPIURL)
|
||||
|
||||
// Partially apply different configs
|
||||
dbCfg := DatabaseConfig{Host: "localhost", Port: 5432}
|
||||
apiCfg := APIConfig{Endpoint: "https://api.example.com", APIKey: "secret"}
|
||||
|
||||
dbQuery := withDBConfig(dbCfg)
|
||||
apiQuery := withAPIConfig(apiCfg)
|
||||
|
||||
// Execute both with the same context
|
||||
ctx := context.Background()
|
||||
|
||||
dbResult := dbQuery(ctx)()
|
||||
apiResult := apiQuery(ctx)()
|
||||
|
||||
if dbURL, err := either.Unwrap(dbResult); err == nil {
|
||||
fmt.Println("Database:", dbURL)
|
||||
}
|
||||
if apiURL, err := either.Unwrap(apiResult); err == nil {
|
||||
fmt.Println("API:", apiURL)
|
||||
}
|
||||
// Output:
|
||||
// Database: localhost:5432
|
||||
// API: https://api.example.com
|
||||
}
|
||||
|
||||
// Example_sequenceReaderResult_errorHandling demonstrates how SequenceReaderResult
|
||||
// enables point-free style with proper error handling at multiple levels.
|
||||
func Example_sequenceReaderResult_errorHandling() {
|
||||
type ValidationConfig struct {
|
||||
MinValue int
|
||||
MaxValue int
|
||||
}
|
||||
|
||||
// A computation that can fail at both outer and inner levels
|
||||
makeValidator := func(ctx context.Context) func() either.Either[error, func(context.Context) either.Either[error, int]] {
|
||||
return func() either.Either[error, func(context.Context) either.Either[error, int]] {
|
||||
// Outer level: check context
|
||||
if ctx.Err() != nil {
|
||||
return either.Left[func(context.Context) either.Either[error, int]](ctx.Err())
|
||||
}
|
||||
|
||||
// Return inner computation
|
||||
return either.Right[error](func(innerCtx context.Context) either.Either[error, int] {
|
||||
// Inner level: perform validation
|
||||
value := 42
|
||||
if value < 0 {
|
||||
return either.Left[int](fmt.Errorf("value too small: %d", value))
|
||||
}
|
||||
if value > 100 {
|
||||
return either.Left[int](fmt.Errorf("value too large: %d", value))
|
||||
}
|
||||
return either.Right[error](value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sequence to enable point-free composition
|
||||
sequenced := RIOE.SequenceReaderResult(makeValidator)
|
||||
|
||||
// Build a pipeline with error handling
|
||||
ctx := context.Background()
|
||||
pipeline := F.Pipe2(
|
||||
sequenced(ctx),
|
||||
RIOE.Map(func(x int) int { return x * 2 }),
|
||||
RIOE.Chain(func(x int) RIOE.ReaderIOResult[string] {
|
||||
return RIOE.Of(fmt.Sprintf("Result: %d", x))
|
||||
}),
|
||||
)
|
||||
|
||||
result := pipeline(ctx)()
|
||||
|
||||
if value, err := either.Unwrap(result); err == nil {
|
||||
fmt.Println(value)
|
||||
}
|
||||
// Output: Result: 84
|
||||
}
|
||||
|
||||
// Example_sequenceReader_partialApplication demonstrates the power of partial
|
||||
// application enabled by SequenceReader for building reusable computations.
|
||||
func Example_sequenceReader_partialApplication() {
|
||||
type ServiceConfig struct {
|
||||
ServiceName string
|
||||
Version string
|
||||
}
|
||||
|
||||
// Create a computation factory
|
||||
makeServiceInfo := func(ctx context.Context) func() either.Either[error, func(ServiceConfig) string] {
|
||||
return func() either.Either[error, func(ServiceConfig) string] {
|
||||
return either.Right[error](func(cfg ServiceConfig) string {
|
||||
return fmt.Sprintf("%s v%s", cfg.ServiceName, cfg.Version)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sequence it
|
||||
sequenced := RIOE.SequenceReader(makeServiceInfo)
|
||||
|
||||
// Create multiple service configurations
|
||||
authConfig := ServiceConfig{ServiceName: "AuthService", Version: "1.0.0"}
|
||||
userConfig := ServiceConfig{ServiceName: "UserService", Version: "2.1.0"}
|
||||
|
||||
// Partially apply each config to create specialized computations
|
||||
getAuthInfo := sequenced(authConfig)
|
||||
getUserInfo := sequenced(userConfig)
|
||||
|
||||
// These can now be reused across different contexts
|
||||
ctx := context.Background()
|
||||
|
||||
authResult := getAuthInfo(ctx)()
|
||||
userResult := getUserInfo(ctx)()
|
||||
|
||||
if auth, err := either.Unwrap(authResult); err == nil {
|
||||
fmt.Println(auth)
|
||||
}
|
||||
if user, err := either.Unwrap(userResult); err == nil {
|
||||
fmt.Println(user)
|
||||
}
|
||||
// Output:
|
||||
// AuthService v1.0.0
|
||||
// UserService v2.1.0
|
||||
}
|
||||
|
||||
// Example_sequenceReader_testingBenefits demonstrates how SequenceReader
|
||||
// makes testing easier by allowing you to inject test dependencies.
|
||||
func Example_sequenceReader_testingBenefits() {
|
||||
// Simple logger that collects messages
|
||||
type SimpleLogger struct {
|
||||
Messages []string
|
||||
}
|
||||
|
||||
// A computation that depends on a logger (using the struct directly)
|
||||
makeLoggingOperation := func(ctx context.Context) func() either.Either[error, func(*SimpleLogger) string] {
|
||||
return func() either.Either[error, func(*SimpleLogger) string] {
|
||||
return either.Right[error](func(logger *SimpleLogger) string {
|
||||
logger.Messages = append(logger.Messages, "Operation started")
|
||||
result := "Success"
|
||||
logger.Messages = append(logger.Messages, fmt.Sprintf("Operation completed: %s", result))
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sequence to enable dependency injection
|
||||
sequenced := RIOE.SequenceReader(makeLoggingOperation)
|
||||
|
||||
// Inject a test logger
|
||||
testLogger := &SimpleLogger{Messages: []string{}}
|
||||
operation := sequenced(testLogger)
|
||||
|
||||
// Execute
|
||||
ctx := context.Background()
|
||||
result := operation(ctx)()
|
||||
|
||||
if value, err := either.Unwrap(result); err == nil {
|
||||
fmt.Println("Result:", value)
|
||||
fmt.Println("Logs:", len(testLogger.Messages))
|
||||
}
|
||||
// Output:
|
||||
// Result: Success
|
||||
// Logs: 2
|
||||
}
|
||||
866
v2/context/readerioresult/flip_test.go
Normal file
866
v2/context/readerioresult/flip_test.go
Normal file
@@ -0,0 +1,866 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSequenceReader(t *testing.T) {
|
||||
t.Run("flips parameter order for simple types", func(t *testing.T) {
|
||||
// Original: ReaderIOResult[Reader[string, int]]
|
||||
// = func(context.Context) func() Either[error, func(string) int]
|
||||
original := func(ctx context.Context) func() Either[Reader[string, int]] {
|
||||
return func() Either[Reader[string, int]] {
|
||||
return either.Right[error](func(s string) int {
|
||||
return 10 + len(s)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sequenced: func(string) func(context.Context) IOResult[int]
|
||||
// The Reader environment (string) is now the first parameter
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test original
|
||||
result1 := original(ctx)()
|
||||
assert.True(t, either.IsRight(result1))
|
||||
innerFunc1, _ := either.Unwrap(result1)
|
||||
value1 := innerFunc1("hello")
|
||||
assert.Equal(t, 15, value1)
|
||||
|
||||
// Test sequenced - note the flipped order: string first, then context
|
||||
result2 := sequenced("hello")(ctx)()
|
||||
assert.True(t, either.IsRight(result2))
|
||||
value2, _ := either.Unwrap(result2)
|
||||
assert.Equal(t, 15, value2)
|
||||
})
|
||||
|
||||
t.Run("flips parameter order for struct types", func(t *testing.T) {
|
||||
type Database struct {
|
||||
ConnectionString string
|
||||
}
|
||||
|
||||
// Original: ReaderIOResult[Reader[Database, string]]
|
||||
query := func(ctx context.Context) func() Either[Reader[Database, string]] {
|
||||
return func() Either[Reader[Database, string]] {
|
||||
if ctx.Err() != nil {
|
||||
return either.Left[Reader[Database, string]](ctx.Err())
|
||||
}
|
||||
return either.Right[error](func(db Database) string {
|
||||
return fmt.Sprintf("Query on %s", db.ConnectionString)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
db := Database{ConnectionString: "localhost:5432"}
|
||||
ctx := context.Background()
|
||||
|
||||
expected := "Query on localhost:5432"
|
||||
|
||||
// Sequence it
|
||||
sequenced := SequenceReader(query)
|
||||
|
||||
// Test original with valid inputs
|
||||
result1 := query(ctx)()
|
||||
assert.True(t, either.IsRight(result1))
|
||||
innerFunc1, _ := either.Unwrap(result1)
|
||||
value1 := innerFunc1(db)
|
||||
assert.Equal(t, expected, value1)
|
||||
|
||||
// Test sequenced with valid inputs - Database first, then context
|
||||
result2 := sequenced(db)(ctx)()
|
||||
assert.True(t, either.IsRight(result2))
|
||||
value2, _ := either.Unwrap(result2)
|
||||
assert.Equal(t, expected, value2)
|
||||
})
|
||||
|
||||
t.Run("preserves outer error", func(t *testing.T) {
|
||||
expectedError := errors.New("outer error")
|
||||
|
||||
// Original that fails at outer level
|
||||
original := func(ctx context.Context) func() Either[Reader[string, int]] {
|
||||
return func() Either[Reader[string, int]] {
|
||||
return either.Left[Reader[string, int]](expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test original with error
|
||||
result1 := original(ctx)()
|
||||
assert.True(t, either.IsLeft(result1))
|
||||
_, err1 := either.Unwrap(result1)
|
||||
assert.Equal(t, expectedError, err1)
|
||||
|
||||
// Test sequenced - the outer error is preserved
|
||||
sequenced := SequenceReader(original)
|
||||
result2 := sequenced("test")(ctx)()
|
||||
assert.True(t, either.IsLeft(result2))
|
||||
_, err2 := either.Unwrap(result2)
|
||||
assert.Equal(t, expectedError, err2)
|
||||
})
|
||||
|
||||
t.Run("preserves computation logic", func(t *testing.T) {
|
||||
// Original function
|
||||
original := func(ctx context.Context) func() Either[Reader[string, int]] {
|
||||
return func() Either[Reader[string, int]] {
|
||||
return either.Right[error](func(s string) int {
|
||||
return 3 * len(s)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Sequence
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
// Test that sequence produces correct results
|
||||
result1 := original(ctx)()
|
||||
innerFunc1, _ := either.Unwrap(result1)
|
||||
value1 := innerFunc1("test")
|
||||
|
||||
result2 := sequenced("test")(ctx)()
|
||||
value2, _ := either.Unwrap(result2)
|
||||
|
||||
assert.Equal(t, value1, value2)
|
||||
assert.Equal(t, 12, value2) // 3 * 4
|
||||
})
|
||||
|
||||
t.Run("works with zero values", func(t *testing.T) {
|
||||
original := func(ctx context.Context) func() Either[Reader[string, int]] {
|
||||
return func() Either[Reader[string, int]] {
|
||||
return either.Right[error](func(s string) int {
|
||||
return len(s)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
// Test with zero values
|
||||
result1 := original(ctx)()
|
||||
innerFunc1, _ := either.Unwrap(result1)
|
||||
value1 := innerFunc1("")
|
||||
assert.Equal(t, 0, value1)
|
||||
|
||||
result2 := sequenced("")(ctx)()
|
||||
value2, _ := either.Unwrap(result2)
|
||||
assert.Equal(t, 0, value2)
|
||||
})
|
||||
|
||||
t.Run("respects context cancellation", func(t *testing.T) {
|
||||
original := func(ctx context.Context) func() Either[Reader[string, int]] {
|
||||
return func() Either[Reader[string, int]] {
|
||||
if ctx.Err() != nil {
|
||||
return either.Left[Reader[string, int]](ctx.Err())
|
||||
}
|
||||
return either.Right[error](func(s string) int {
|
||||
return len(s)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
result := sequenced("test")(ctx)()
|
||||
assert.True(t, either.IsLeft(result))
|
||||
_, err := either.Unwrap(result)
|
||||
assert.Equal(t, context.Canceled, err)
|
||||
})
|
||||
|
||||
t.Run("enables point-free style with partial application", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Multiplier int
|
||||
}
|
||||
|
||||
// Original computation
|
||||
original := func(ctx context.Context) func() Either[Reader[Config, int]] {
|
||||
return func() Either[Reader[Config, int]] {
|
||||
return either.Right[error](func(cfg Config) int {
|
||||
return cfg.Multiplier * 10
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sequence to enable partial application
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
// Partially apply the Config
|
||||
cfg := Config{Multiplier: 5}
|
||||
withConfig := sequenced(cfg)
|
||||
|
||||
// Now we have a ReaderIOResult[int] that can be used in different contexts
|
||||
ctx1 := context.Background()
|
||||
result1 := withConfig(ctx1)()
|
||||
assert.True(t, either.IsRight(result1))
|
||||
value1, _ := either.Unwrap(result1)
|
||||
assert.Equal(t, 50, value1)
|
||||
|
||||
// Can reuse with different context
|
||||
ctx2 := context.Background()
|
||||
result2 := withConfig(ctx2)()
|
||||
assert.True(t, either.IsRight(result2))
|
||||
value2, _ := either.Unwrap(result2)
|
||||
assert.Equal(t, 50, value2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSequenceReaderIO(t *testing.T) {
|
||||
t.Run("flips parameter order for simple types", func(t *testing.T) {
|
||||
// Original: ReaderIOResult[ReaderIO[int]]
|
||||
// = func(context.Context) func() Either[error, func(context.Context) func() int]
|
||||
original := func(ctx context.Context) func() Either[ReaderIO[int]] {
|
||||
return func() Either[ReaderIO[int]] {
|
||||
return either.Right[error](func(innerCtx context.Context) func() int {
|
||||
return func() int {
|
||||
return 20
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
sequenced := SequenceReaderIO(original)
|
||||
|
||||
// Test original
|
||||
result1 := original(ctx)()
|
||||
assert.True(t, either.IsRight(result1))
|
||||
innerFunc1, _ := either.Unwrap(result1)
|
||||
value1 := innerFunc1(ctx)()
|
||||
assert.Equal(t, 20, value1)
|
||||
|
||||
// Test sequenced - context first, then context again for inner ReaderIO
|
||||
result2 := sequenced(ctx)(ctx)()
|
||||
assert.True(t, either.IsRight(result2))
|
||||
value2, _ := either.Unwrap(result2)
|
||||
assert.Equal(t, 20, value2)
|
||||
})
|
||||
|
||||
t.Run("preserves outer error", func(t *testing.T) {
|
||||
expectedError := errors.New("outer error")
|
||||
|
||||
// Original that fails at outer level
|
||||
original := func(ctx context.Context) func() Either[ReaderIO[int]] {
|
||||
return func() Either[ReaderIO[int]] {
|
||||
return either.Left[ReaderIO[int]](expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test original with error
|
||||
result1 := original(ctx)()
|
||||
assert.True(t, either.IsLeft(result1))
|
||||
_, err1 := either.Unwrap(result1)
|
||||
assert.Equal(t, expectedError, err1)
|
||||
|
||||
// Test sequenced - the outer error is preserved
|
||||
sequenced := SequenceReaderIO(original)
|
||||
result2 := sequenced(ctx)(ctx)()
|
||||
assert.True(t, either.IsLeft(result2))
|
||||
_, err2 := either.Unwrap(result2)
|
||||
assert.Equal(t, expectedError, err2)
|
||||
})
|
||||
|
||||
t.Run("respects context cancellation in outer context", func(t *testing.T) {
|
||||
original := func(ctx context.Context) func() Either[ReaderIO[int]] {
|
||||
return func() Either[ReaderIO[int]] {
|
||||
if ctx.Err() != nil {
|
||||
return either.Left[ReaderIO[int]](ctx.Err())
|
||||
}
|
||||
return either.Right[error](func(innerCtx context.Context) func() int {
|
||||
return func() int {
|
||||
return 20
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
sequenced := SequenceReaderIO(original)
|
||||
|
||||
result := sequenced(ctx)(ctx)()
|
||||
assert.True(t, either.IsLeft(result))
|
||||
_, err := either.Unwrap(result)
|
||||
assert.Equal(t, context.Canceled, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSequenceReaderResult(t *testing.T) {
|
||||
t.Run("flips parameter order for simple types", func(t *testing.T) {
|
||||
// Original: ReaderIOResult[ReaderResult[int]]
|
||||
// = func(context.Context) func() Either[error, func(context.Context) Either[error, int]]
|
||||
original := func(ctx context.Context) func() Either[ReaderResult[int]] {
|
||||
return func() Either[ReaderResult[int]] {
|
||||
return either.Right[error](func(innerCtx context.Context) Either[int] {
|
||||
return either.Right[error](20)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
sequenced := SequenceReaderResult(original)
|
||||
|
||||
// Test original
|
||||
result1 := original(ctx)()
|
||||
assert.True(t, either.IsRight(result1))
|
||||
innerFunc1, _ := either.Unwrap(result1)
|
||||
innerResult1 := innerFunc1(ctx)
|
||||
assert.True(t, either.IsRight(innerResult1))
|
||||
value1, _ := either.Unwrap(innerResult1)
|
||||
assert.Equal(t, 20, value1)
|
||||
|
||||
// Test sequenced
|
||||
result2 := sequenced(ctx)(ctx)()
|
||||
assert.True(t, either.IsRight(result2))
|
||||
value2, _ := either.Unwrap(result2)
|
||||
assert.Equal(t, 20, value2)
|
||||
})
|
||||
|
||||
t.Run("preserves outer error", func(t *testing.T) {
|
||||
expectedError := errors.New("outer error")
|
||||
|
||||
// Original that fails at outer level
|
||||
original := func(ctx context.Context) func() Either[ReaderResult[int]] {
|
||||
return func() Either[ReaderResult[int]] {
|
||||
return either.Left[ReaderResult[int]](expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test original with error
|
||||
result1 := original(ctx)()
|
||||
assert.True(t, either.IsLeft(result1))
|
||||
_, err1 := either.Unwrap(result1)
|
||||
assert.Equal(t, expectedError, err1)
|
||||
|
||||
// Test sequenced - the outer error is preserved
|
||||
sequenced := SequenceReaderResult(original)
|
||||
result2 := sequenced(ctx)(ctx)()
|
||||
assert.True(t, either.IsLeft(result2))
|
||||
_, err2 := either.Unwrap(result2)
|
||||
assert.Equal(t, expectedError, err2)
|
||||
})
|
||||
|
||||
t.Run("preserves inner error", func(t *testing.T) {
|
||||
expectedError := errors.New("inner error")
|
||||
|
||||
// Original that fails at inner level
|
||||
original := func(ctx context.Context) func() Either[ReaderResult[int]] {
|
||||
return func() Either[ReaderResult[int]] {
|
||||
return either.Right[error](func(innerCtx context.Context) Either[int] {
|
||||
return either.Left[int](expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test original with inner error
|
||||
result1 := original(ctx)()
|
||||
assert.True(t, either.IsRight(result1))
|
||||
innerFunc1, _ := either.Unwrap(result1)
|
||||
innerResult1 := innerFunc1(ctx)
|
||||
assert.True(t, either.IsLeft(innerResult1))
|
||||
_, innerErr1 := either.Unwrap(innerResult1)
|
||||
assert.Equal(t, expectedError, innerErr1)
|
||||
|
||||
// Test sequenced with inner error
|
||||
sequenced := SequenceReaderResult(original)
|
||||
result2 := sequenced(ctx)(ctx)()
|
||||
assert.True(t, either.IsLeft(result2))
|
||||
_, innerErr2 := either.Unwrap(result2)
|
||||
assert.Equal(t, expectedError, innerErr2)
|
||||
})
|
||||
|
||||
t.Run("handles errors at different levels", func(t *testing.T) {
|
||||
// Original that can fail at both levels
|
||||
makeOriginal := func(x int) ReaderIOResult[ReaderResult[int]] {
|
||||
return func(ctx context.Context) func() Either[ReaderResult[int]] {
|
||||
return func() Either[ReaderResult[int]] {
|
||||
if x < -10 {
|
||||
return either.Left[ReaderResult[int]](errors.New("outer: too negative"))
|
||||
}
|
||||
return either.Right[error](func(innerCtx context.Context) Either[int] {
|
||||
if x < 0 {
|
||||
return either.Left[int](errors.New("inner: negative value"))
|
||||
}
|
||||
return either.Right[error](x * 2)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test outer error
|
||||
sequenced1 := SequenceReaderResult(makeOriginal(-20))
|
||||
result1 := sequenced1(ctx)(ctx)()
|
||||
assert.True(t, either.IsLeft(result1))
|
||||
_, err1 := either.Unwrap(result1)
|
||||
assert.Contains(t, err1.Error(), "outer")
|
||||
|
||||
// Test inner error
|
||||
sequenced2 := SequenceReaderResult(makeOriginal(-5))
|
||||
result2 := sequenced2(ctx)(ctx)()
|
||||
assert.True(t, either.IsLeft(result2))
|
||||
_, err2 := either.Unwrap(result2)
|
||||
assert.Contains(t, err2.Error(), "inner")
|
||||
|
||||
// Test success
|
||||
sequenced3 := SequenceReaderResult(makeOriginal(10))
|
||||
result3 := sequenced3(ctx)(ctx)()
|
||||
assert.True(t, either.IsRight(result3))
|
||||
value3, _ := either.Unwrap(result3)
|
||||
assert.Equal(t, 20, value3)
|
||||
})
|
||||
|
||||
t.Run("respects context cancellation", func(t *testing.T) {
|
||||
original := func(ctx context.Context) func() Either[ReaderResult[int]] {
|
||||
return func() Either[ReaderResult[int]] {
|
||||
if ctx.Err() != nil {
|
||||
return either.Left[ReaderResult[int]](ctx.Err())
|
||||
}
|
||||
return either.Right[error](func(innerCtx context.Context) Either[int] {
|
||||
if innerCtx.Err() != nil {
|
||||
return either.Left[int](innerCtx.Err())
|
||||
}
|
||||
return either.Right[error](20)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
sequenced := SequenceReaderResult(original)
|
||||
|
||||
result := sequenced(ctx)(ctx)()
|
||||
assert.True(t, either.IsLeft(result))
|
||||
_, err := either.Unwrap(result)
|
||||
assert.Equal(t, context.Canceled, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSequenceEdgeCases(t *testing.T) {
|
||||
t.Run("works with empty struct", func(t *testing.T) {
|
||||
type Empty struct{}
|
||||
|
||||
original := func(ctx context.Context) func() Either[Reader[Empty, int]] {
|
||||
return func() Either[Reader[Empty, int]] {
|
||||
return either.Right[error](func(e Empty) int {
|
||||
return 20
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
empty := Empty{}
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
result1 := original(ctx)()
|
||||
innerFunc1, _ := either.Unwrap(result1)
|
||||
value1 := innerFunc1(empty)
|
||||
assert.Equal(t, 20, value1)
|
||||
|
||||
result2 := sequenced(empty)(ctx)()
|
||||
value2, _ := either.Unwrap(result2)
|
||||
assert.Equal(t, 20, value2)
|
||||
})
|
||||
|
||||
t.Run("works with pointer types", func(t *testing.T) {
|
||||
type Data struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
original := func(ctx context.Context) func() Either[Reader[*Data, int]] {
|
||||
return func() Either[Reader[*Data, int]] {
|
||||
return either.Right[error](func(d *Data) int {
|
||||
if d == nil {
|
||||
return 42
|
||||
}
|
||||
return 42 + d.Value
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
data := &Data{Value: 100}
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
// Test with non-nil pointer
|
||||
result1 := original(ctx)()
|
||||
innerFunc1, _ := either.Unwrap(result1)
|
||||
value1 := innerFunc1(data)
|
||||
assert.Equal(t, 142, value1)
|
||||
|
||||
result2 := sequenced(data)(ctx)()
|
||||
value2, _ := either.Unwrap(result2)
|
||||
assert.Equal(t, 142, value2)
|
||||
|
||||
// Test with nil pointer
|
||||
result3 := sequenced(nil)(ctx)()
|
||||
value3, _ := either.Unwrap(result3)
|
||||
assert.Equal(t, 42, value3)
|
||||
})
|
||||
|
||||
t.Run("maintains referential transparency", func(t *testing.T) {
|
||||
// The same inputs should always produce the same outputs
|
||||
original := func(ctx context.Context) func() Either[Reader[string, int]] {
|
||||
return func() Either[Reader[string, int]] {
|
||||
return either.Right[error](func(s string) int {
|
||||
return 10 + len(s)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
// Call multiple times with same inputs
|
||||
for range 5 {
|
||||
result1 := original(ctx)()
|
||||
innerFunc1, _ := either.Unwrap(result1)
|
||||
value1 := innerFunc1("hello")
|
||||
assert.Equal(t, 15, value1)
|
||||
|
||||
result2 := sequenced("hello")(ctx)()
|
||||
value2, _ := either.Unwrap(result2)
|
||||
assert.Equal(t, 15, 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 := Right(10)
|
||||
|
||||
// Reader-based transformation
|
||||
multiply := func(x int) Reader[Config, int] {
|
||||
return func(cfg Config) int {
|
||||
return x * cfg.Multiplier
|
||||
}
|
||||
}
|
||||
|
||||
// Apply TraverseReader
|
||||
traversed := TraverseReader(multiply)
|
||||
result := traversed(original)
|
||||
|
||||
// Provide Config and execute
|
||||
cfg := Config{Multiplier: 5}
|
||||
ctx := context.Background()
|
||||
finalResult := result(cfg)(ctx)()
|
||||
|
||||
assert.True(t, either.IsRight(finalResult))
|
||||
value, _ := either.Unwrap(finalResult)
|
||||
assert.Equal(t, 50, value)
|
||||
})
|
||||
|
||||
t.Run("preserves outer error", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Multiplier int
|
||||
}
|
||||
|
||||
expectedError := errors.New("computation failed")
|
||||
|
||||
// Original computation that fails
|
||||
original := Left[int](expectedError)
|
||||
|
||||
// Reader-based transformation (won't be called)
|
||||
multiply := func(x int) Reader[Config, int] {
|
||||
return func(cfg Config) int {
|
||||
return x * cfg.Multiplier
|
||||
}
|
||||
}
|
||||
|
||||
// Apply TraverseReader
|
||||
traversed := TraverseReader(multiply)
|
||||
result := traversed(original)
|
||||
|
||||
// Provide Config and execute
|
||||
cfg := Config{Multiplier: 5}
|
||||
ctx := context.Background()
|
||||
finalResult := result(cfg)(ctx)()
|
||||
|
||||
assert.True(t, either.IsLeft(finalResult))
|
||||
_, err := either.Unwrap(finalResult)
|
||||
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 := Right(42)
|
||||
|
||||
// Reader-based transformation: int -> string using Database
|
||||
format := func(x int) func(Database) string {
|
||||
return func(db Database) string {
|
||||
return fmt.Sprintf("%s:%d", db.Prefix, x)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply TraverseReader
|
||||
traversed := TraverseReader(format)
|
||||
result := traversed(original)
|
||||
|
||||
// Provide Database and execute
|
||||
db := Database{Prefix: "ID"}
|
||||
ctx := context.Background()
|
||||
finalResult := result(db)(ctx)()
|
||||
|
||||
assert.True(t, either.IsRight(finalResult))
|
||||
value, _ := either.Unwrap(finalResult)
|
||||
assert.Equal(t, "ID:42", value)
|
||||
})
|
||||
|
||||
t.Run("works with struct environments", func(t *testing.T) {
|
||||
type Settings struct {
|
||||
Prefix string
|
||||
Suffix string
|
||||
}
|
||||
|
||||
// Original computation
|
||||
original := Right("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(decorate)
|
||||
result := traversed(original)
|
||||
|
||||
// Provide Settings and execute
|
||||
settings := Settings{Prefix: "[", Suffix: "]"}
|
||||
ctx := context.Background()
|
||||
finalResult := result(settings)(ctx)()
|
||||
|
||||
assert.True(t, either.IsRight(finalResult))
|
||||
value, _ := either.Unwrap(finalResult)
|
||||
assert.Equal(t, "[value]", value)
|
||||
})
|
||||
|
||||
t.Run("enables partial application", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Factor int
|
||||
}
|
||||
|
||||
// Original computation
|
||||
original := Right(10)
|
||||
|
||||
// Reader-based transformation
|
||||
scale := func(x int) Reader[Config, int] {
|
||||
return func(cfg Config) int {
|
||||
return x * cfg.Factor
|
||||
}
|
||||
}
|
||||
|
||||
// Apply TraverseReader
|
||||
traversed := TraverseReader(scale)
|
||||
result := traversed(original)
|
||||
|
||||
// Partially apply Config
|
||||
cfg := Config{Factor: 3}
|
||||
withConfig := result(cfg)
|
||||
|
||||
// Can now use with different contexts
|
||||
ctx1 := context.Background()
|
||||
finalResult1 := withConfig(ctx1)()
|
||||
assert.True(t, either.IsRight(finalResult1))
|
||||
value1, _ := either.Unwrap(finalResult1)
|
||||
assert.Equal(t, 30, value1)
|
||||
|
||||
// Reuse with different context
|
||||
ctx2 := context.Background()
|
||||
finalResult2 := withConfig(ctx2)()
|
||||
assert.True(t, either.IsRight(finalResult2))
|
||||
value2, _ := either.Unwrap(finalResult2)
|
||||
assert.Equal(t, 30, value2)
|
||||
})
|
||||
|
||||
t.Run("respects context cancellation", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
// Original computation that checks context
|
||||
original := func(ctx context.Context) func() Either[int] {
|
||||
return func() Either[int] {
|
||||
if ctx.Err() != nil {
|
||||
return either.Left[int](ctx.Err())
|
||||
}
|
||||
return either.Right[error](10)
|
||||
}
|
||||
}
|
||||
|
||||
// Reader-based transformation
|
||||
multiply := func(x int) Reader[Config, int] {
|
||||
return func(cfg Config) int {
|
||||
return x * cfg.Value
|
||||
}
|
||||
}
|
||||
|
||||
// Apply TraverseReader
|
||||
traversed := TraverseReader(multiply)
|
||||
result := traversed(original)
|
||||
|
||||
// Use canceled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
cfg := Config{Value: 5}
|
||||
finalResult := result(cfg)(ctx)()
|
||||
|
||||
assert.True(t, either.IsLeft(finalResult))
|
||||
_, err := either.Unwrap(finalResult)
|
||||
assert.Equal(t, context.Canceled, err)
|
||||
})
|
||||
|
||||
t.Run("works with zero values", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Offset int
|
||||
}
|
||||
|
||||
// Original computation with zero value
|
||||
original := Right(0)
|
||||
|
||||
// Reader-based transformation
|
||||
add := func(x int) Reader[Config, int] {
|
||||
return func(cfg Config) int {
|
||||
return x + cfg.Offset
|
||||
}
|
||||
}
|
||||
|
||||
// Apply TraverseReader
|
||||
traversed := TraverseReader(add)
|
||||
result := traversed(original)
|
||||
|
||||
// Provide Config with zero offset
|
||||
cfg := Config{Offset: 0}
|
||||
ctx := context.Background()
|
||||
finalResult := result(cfg)(ctx)()
|
||||
|
||||
assert.True(t, either.IsRight(finalResult))
|
||||
value, _ := either.Unwrap(finalResult)
|
||||
assert.Equal(t, 0, value)
|
||||
})
|
||||
|
||||
t.Run("chains multiple transformations", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Multiplier int
|
||||
}
|
||||
|
||||
// Original computation
|
||||
original := Right(5)
|
||||
|
||||
// First Reader-based transformation
|
||||
multiply := func(x int) Reader[Config, int] {
|
||||
return func(cfg Config) int {
|
||||
return x * cfg.Multiplier
|
||||
}
|
||||
}
|
||||
|
||||
// Apply TraverseReader
|
||||
traversed := TraverseReader(multiply)
|
||||
result := traversed(original)
|
||||
|
||||
// Provide Config and execute
|
||||
cfg := Config{Multiplier: 4}
|
||||
ctx := context.Background()
|
||||
finalResult := result(cfg)(ctx)()
|
||||
|
||||
assert.True(t, either.IsRight(finalResult))
|
||||
value, _ := either.Unwrap(finalResult)
|
||||
assert.Equal(t, 20, value) // 5 * 4 = 20
|
||||
})
|
||||
|
||||
t.Run("works with complex Reader logic", func(t *testing.T) {
|
||||
type ValidationRules struct {
|
||||
MinValue int
|
||||
MaxValue int
|
||||
}
|
||||
|
||||
// Original computation
|
||||
original := Right(50)
|
||||
|
||||
// Reader-based transformation with validation logic
|
||||
validate := func(x int) func(ValidationRules) int {
|
||||
return func(rules ValidationRules) int {
|
||||
if x < rules.MinValue {
|
||||
return rules.MinValue
|
||||
}
|
||||
if x > rules.MaxValue {
|
||||
return rules.MaxValue
|
||||
}
|
||||
return x
|
||||
}
|
||||
}
|
||||
|
||||
// Apply TraverseReader
|
||||
traversed := TraverseReader(validate)
|
||||
result := traversed(original)
|
||||
|
||||
// Test with value within range
|
||||
rules1 := ValidationRules{MinValue: 0, MaxValue: 100}
|
||||
ctx := context.Background()
|
||||
finalResult1 := result(rules1)(ctx)()
|
||||
assert.True(t, either.IsRight(finalResult1))
|
||||
value1, _ := either.Unwrap(finalResult1)
|
||||
assert.Equal(t, 50, value1)
|
||||
|
||||
// Test with value above max
|
||||
rules2 := ValidationRules{MinValue: 0, MaxValue: 30}
|
||||
finalResult2 := result(rules2)(ctx)()
|
||||
assert.True(t, either.IsRight(finalResult2))
|
||||
value2, _ := either.Unwrap(finalResult2)
|
||||
assert.Equal(t, 30, value2) // Clamped to max
|
||||
|
||||
// Test with value below min
|
||||
rules3 := ValidationRules{MinValue: 60, MaxValue: 100}
|
||||
finalResult3 := result(rules3)(ctx)()
|
||||
assert.True(t, either.IsRight(finalResult3))
|
||||
value3, _ := either.Unwrap(finalResult3)
|
||||
assert.Equal(t, 60, value3) // Clamped to min
|
||||
})
|
||||
}
|
||||
@@ -73,7 +73,7 @@ type (
|
||||
// It wraps a standard http.Client and provides functional HTTP operations.
|
||||
client struct {
|
||||
delegate *http.Client
|
||||
doIOE func(*http.Request) IOE.IOEither[error, *http.Response]
|
||||
doIOE IOE.Kleisli[error, *http.Request, *http.Response]
|
||||
}
|
||||
)
|
||||
|
||||
@@ -158,7 +158,7 @@ func MakeClient(httpClient *http.Client) Client {
|
||||
// request := MakeGetRequest("https://api.example.com/data")
|
||||
// fullResp := ReadFullResponse(client)(request)
|
||||
// result := fullResp(context.Background())()
|
||||
func ReadFullResponse(client Client) func(Requester) RIOE.ReaderIOResult[H.FullResponse] {
|
||||
func ReadFullResponse(client Client) RIOE.Kleisli[Requester, H.FullResponse] {
|
||||
return func(req Requester) RIOE.ReaderIOResult[H.FullResponse] {
|
||||
return F.Flow3(
|
||||
client.Do(req),
|
||||
@@ -195,7 +195,7 @@ func ReadFullResponse(client Client) func(Requester) RIOE.ReaderIOResult[H.FullR
|
||||
// request := MakeGetRequest("https://api.example.com/data")
|
||||
// readBytes := ReadAll(client)
|
||||
// result := readBytes(request)(context.Background())()
|
||||
func ReadAll(client Client) func(Requester) RIOE.ReaderIOResult[[]byte] {
|
||||
func ReadAll(client Client) RIOE.Kleisli[Requester, []byte] {
|
||||
return F.Flow2(
|
||||
ReadFullResponse(client),
|
||||
RIOE.Map(H.Body),
|
||||
@@ -219,7 +219,7 @@ func ReadAll(client Client) func(Requester) RIOE.ReaderIOResult[[]byte] {
|
||||
// request := MakeGetRequest("https://api.example.com/text")
|
||||
// readText := ReadText(client)
|
||||
// result := readText(request)(context.Background())()
|
||||
func ReadText(client Client) func(Requester) RIOE.ReaderIOResult[string] {
|
||||
func ReadText(client Client) RIOE.Kleisli[Requester, string] {
|
||||
return F.Flow2(
|
||||
ReadAll(client),
|
||||
RIOE.Map(B.ToString),
|
||||
@@ -231,7 +231,7 @@ func ReadText(client Client) func(Requester) RIOE.ReaderIOResult[string] {
|
||||
// Deprecated: Use [ReadJSON] instead. This function is kept for backward compatibility
|
||||
// but will be removed in a future version. The capitalized version follows Go naming
|
||||
// conventions for acronyms.
|
||||
func ReadJson[A any](client Client) func(Requester) RIOE.ReaderIOResult[A] {
|
||||
func ReadJson[A any](client Client) RIOE.Kleisli[Requester, A] {
|
||||
return ReadJSON[A](client)
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@ func ReadJson[A any](client Client) func(Requester) RIOE.ReaderIOResult[A] {
|
||||
// 3. Reads the response body as bytes
|
||||
//
|
||||
// This function is used internally by ReadJSON to ensure proper JSON response handling.
|
||||
func readJSON(client Client) func(Requester) RIOE.ReaderIOResult[[]byte] {
|
||||
func readJSON(client Client) RIOE.Kleisli[Requester, []byte] {
|
||||
return F.Flow3(
|
||||
ReadFullResponse(client),
|
||||
RIOE.ChainFirstEitherK(F.Flow2(
|
||||
@@ -278,7 +278,7 @@ func readJSON(client Client) func(Requester) RIOE.ReaderIOResult[[]byte] {
|
||||
// request := MakeGetRequest("https://api.example.com/user/1")
|
||||
// readUser := ReadJSON[User](client)
|
||||
// result := readUser(request)(context.Background())()
|
||||
func ReadJSON[A any](client Client) func(Requester) RIOE.ReaderIOResult[A] {
|
||||
func ReadJSON[A any](client Client) RIOE.Kleisli[Requester, A] {
|
||||
return F.Flow2(
|
||||
readJSON(client),
|
||||
RIOE.ChainEitherK(J.Unmarshal[A]),
|
||||
|
||||
732
v2/context/readerioresult/logging.go
Normal file
732
v2/context/readerioresult/logging.go
Normal file
@@ -0,0 +1,732 @@
|
||||
// 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 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/slog"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/readerio"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/logging"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"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
|
||||
|
||||
// loggingContext holds the logging state for a computation, including timing,
|
||||
// correlation ID, logger instance, and whether logging is enabled.
|
||||
loggingContext struct {
|
||||
contextID LoggingID // Unique identifier for this logged operation
|
||||
startTime time.Time // When the operation started (for duration calculation)
|
||||
logger *slog.Logger // The logger instance to use for this operation
|
||||
isEnabled bool // Whether logging is enabled for this operation
|
||||
}
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
loggingContextValue = F.Bind2nd(context.Context.Value, any(loggingContextKey))
|
||||
|
||||
withLoggingContextValue = F.Bind2of3(context.WithValue)(any(loggingContextKey))
|
||||
|
||||
// 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 = F.Flow3(
|
||||
loggingContextValue,
|
||||
option.ToType[loggingContext],
|
||||
option.GetOrElse(getDefaultLoggingContext),
|
||||
)
|
||||
)
|
||||
|
||||
// getDefaultLoggingContext returns a default logging context with the global logger.
|
||||
// This is used when no logging context is found in the context.Context.
|
||||
func getDefaultLoggingContext() loggingContext {
|
||||
return loggingContext{
|
||||
logger: logging.GetLogger(),
|
||||
}
|
||||
}
|
||||
|
||||
// withLoggingContext creates an endomorphism that adds a logging context to a context.Context.
|
||||
// This is used internally to store logging state in the context for retrieval by nested operations.
|
||||
//
|
||||
// Parameters:
|
||||
// - lctx: The logging context to store
|
||||
//
|
||||
// Returns:
|
||||
// - An endomorphism that adds the logging context to a context.Context
|
||||
func withLoggingContext(lctx loggingContext) Endomorphism[context.Context] {
|
||||
return F.Bind2nd(withLoggingContextValue, any(lctx))
|
||||
}
|
||||
|
||||
// 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 F.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 F.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 := F.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(F.Flow2(
|
||||
src,
|
||||
FromIOResult,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// onEntry creates a ReaderIO that handles the entry logging for an operation.
|
||||
// It generates a unique logging ID, captures the start time, and logs the entry message.
|
||||
// The logging context is stored in the context.Context for later retrieval.
|
||||
//
|
||||
// Parameters:
|
||||
// - logLevel: The slog.Level to use for logging (e.g., slog.LevelInfo, slog.LevelDebug)
|
||||
// - cb: Callback function to retrieve the logger from the context
|
||||
// - nameAttr: The slog.Attr containing the operation name
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderIO that prepares the context with logging information and logs the entry
|
||||
func onEntry(
|
||||
logLevel slog.Level,
|
||||
cb func(context.Context) *slog.Logger,
|
||||
nameAttr slog.Attr,
|
||||
) ReaderIO[context.Context] {
|
||||
|
||||
return func(ctx context.Context) IO[context.Context] {
|
||||
// logger
|
||||
logger := cb(ctx)
|
||||
|
||||
return func() context.Context {
|
||||
// check if the logger is enabled
|
||||
if logger.Enabled(ctx, logLevel) {
|
||||
// Generate unique logging ID and capture start time
|
||||
contextID := LoggingID(loggingCounter.Add(1))
|
||||
startTime := time.Now()
|
||||
|
||||
newLogger := logger.With("ID", contextID)
|
||||
|
||||
// log using ID
|
||||
newLogger.LogAttrs(ctx, logLevel, "[entering]", nameAttr)
|
||||
|
||||
withCtx := withLoggingContext(loggingContext{
|
||||
contextID: contextID,
|
||||
startTime: startTime,
|
||||
logger: newLogger,
|
||||
isEnabled: true,
|
||||
})
|
||||
withLogger := logging.WithLogger(newLogger)
|
||||
|
||||
return withCtx(withLogger(ctx))
|
||||
}
|
||||
// logging disabled
|
||||
withCtx := withLoggingContext(loggingContext{
|
||||
logger: logger,
|
||||
isEnabled: false,
|
||||
})
|
||||
return withCtx(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// onExitAny creates a Kleisli function that handles exit logging for an operation.
|
||||
// It logs either success or error based on the Result, including the operation duration.
|
||||
// Only logs if logging was enabled during entry (checked via loggingContext.isEnabled).
|
||||
//
|
||||
// Parameters:
|
||||
// - logLevel: The slog.Level to use for logging
|
||||
// - nameAttr: The slog.Attr containing the operation name
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli function that logs the exit/error and returns nil
|
||||
func onExitAny(
|
||||
logLevel slog.Level,
|
||||
nameAttr slog.Attr,
|
||||
) readerio.Kleisli[Result[any], any] {
|
||||
return func(res Result[any]) ReaderIO[any] {
|
||||
return func(ctx context.Context) IO[any] {
|
||||
value := getLoggingContext(ctx)
|
||||
|
||||
if value.isEnabled {
|
||||
|
||||
return func() any {
|
||||
// Retrieve logging information from context
|
||||
durationAttr := slog.Duration("duration", time.Since(value.startTime))
|
||||
|
||||
// Log error with ID and duration
|
||||
onError := func(err error) any {
|
||||
value.logger.LogAttrs(ctx, logLevel, "[throwing]",
|
||||
nameAttr,
|
||||
durationAttr,
|
||||
slog.Any("error", err))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Log success with ID and duration
|
||||
onSuccess := func(_ any) any {
|
||||
value.logger.LogAttrs(ctx, logLevel, "[exiting ]", nameAttr, durationAttr)
|
||||
return nil
|
||||
}
|
||||
|
||||
return F.Pipe1(
|
||||
res,
|
||||
result.Fold(onError, onSuccess),
|
||||
)
|
||||
}
|
||||
}
|
||||
// nothing to do
|
||||
return io.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LogEntryExitWithCallback creates an operator that logs entry and exit of a ReaderIOResult computation
|
||||
// using a custom logger callback and log level. This provides more control than LogEntryExit.
|
||||
//
|
||||
// This function allows you to:
|
||||
// - Use a custom log level (Debug, Info, Warn, Error)
|
||||
// - Retrieve the logger from the context using a custom callback
|
||||
// - Control whether logging is enabled based on the logger's configuration
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type of the ReaderIOResult
|
||||
//
|
||||
// Parameters:
|
||||
// - logLevel: The slog.Level to use for all log messages (entry, exit, error)
|
||||
// - cb: Callback function to retrieve the *slog.Logger from the context
|
||||
// - name: A descriptive name for the operation
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that wraps the ReaderIOResult with customizable logging
|
||||
//
|
||||
// Example with custom log level:
|
||||
//
|
||||
// // Log at debug level
|
||||
// debugOp := LogEntryExitWithCallback[User](
|
||||
// slog.LevelDebug,
|
||||
// logging.GetLoggerFromContext,
|
||||
// "fetchUser",
|
||||
// )
|
||||
// result := debugOp(fetchUser(123))
|
||||
//
|
||||
// Example with custom logger callback:
|
||||
//
|
||||
// type loggerKey int
|
||||
// const myLoggerKey loggerKey = 0
|
||||
//
|
||||
// getMyLogger := func(ctx context.Context) *slog.Logger {
|
||||
// if logger := ctx.Value(myLoggerKey); logger != nil {
|
||||
// return logger.(*slog.Logger)
|
||||
// }
|
||||
// return slog.Default()
|
||||
// }
|
||||
//
|
||||
// customOp := LogEntryExitWithCallback[Data](
|
||||
// slog.LevelInfo,
|
||||
// getMyLogger,
|
||||
// "processData",
|
||||
// )
|
||||
func LogEntryExitWithCallback[A any](
|
||||
logLevel slog.Level,
|
||||
cb func(context.Context) *slog.Logger,
|
||||
name string) Operator[A, A] {
|
||||
|
||||
nameAttr := slog.String("name", name)
|
||||
|
||||
return LogEntryExitF(
|
||||
onEntry(logLevel, cb, nameAttr),
|
||||
F.Flow2(
|
||||
result.MapTo[A, any](nil),
|
||||
onExitAny(logLevel, nameAttr),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// 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]
|
||||
// // [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
|
||||
//
|
||||
//go:inline
|
||||
func LogEntryExit[A any](name string) Operator[A, A] {
|
||||
return LogEntryExitWithCallback[A](slog.LevelInfo, logging.GetLoggerFromContext, name)
|
||||
}
|
||||
|
||||
func curriedLog(
|
||||
logLevel slog.Level,
|
||||
cb func(context.Context) *slog.Logger,
|
||||
message string) func(slog.Attr) func(context.Context) func() struct{} {
|
||||
return F.Curry2(func(a slog.Attr, ctx context.Context) func() struct{} {
|
||||
logger := cb(ctx)
|
||||
return func() struct{} {
|
||||
logger.LogAttrs(ctx, logLevel, message, a)
|
||||
return struct{}{}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// SLogWithCallback creates a Kleisli arrow that logs a Result value (success or error) with a custom logger and log level.
|
||||
//
|
||||
// This function logs both successful values and errors, making it useful for debugging and monitoring
|
||||
// Result values as they flow through a computation. Unlike TapSLog which only logs successful values,
|
||||
// SLogWithCallback logs the Result regardless of whether it contains a value or an error.
|
||||
//
|
||||
// The logged output includes:
|
||||
// - For success: The message with the value as a structured "value" attribute
|
||||
// - For error: The message with the error as a structured "error" attribute
|
||||
//
|
||||
// The Result is passed through unchanged after logging.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type of the Result
|
||||
//
|
||||
// Parameters:
|
||||
// - logLevel: The slog.Level to use for logging (e.g., slog.LevelInfo, slog.LevelDebug)
|
||||
// - cb: Callback function to retrieve the *slog.Logger from the context
|
||||
// - message: A descriptive message to include in the log entry
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that logs the Result (value or error) and returns it unchanged
|
||||
//
|
||||
// Example with custom log level:
|
||||
//
|
||||
// debugLog := SLogWithCallback[User](
|
||||
// slog.LevelDebug,
|
||||
// logging.GetLoggerFromContext,
|
||||
// "User result",
|
||||
// )
|
||||
//
|
||||
// pipeline := F.Pipe2(
|
||||
// fetchUser(123),
|
||||
// Chain(debugLog),
|
||||
// Map(func(u User) string { return u.Name }),
|
||||
// )
|
||||
//
|
||||
// Example with custom logger:
|
||||
//
|
||||
// type loggerKey int
|
||||
// const myLoggerKey loggerKey = 0
|
||||
//
|
||||
// getMyLogger := func(ctx context.Context) *slog.Logger {
|
||||
// if logger := ctx.Value(myLoggerKey); logger != nil {
|
||||
// return logger.(*slog.Logger)
|
||||
// }
|
||||
// return slog.Default()
|
||||
// }
|
||||
//
|
||||
// customLog := SLogWithCallback[Data](
|
||||
// slog.LevelWarn,
|
||||
// getMyLogger,
|
||||
// "Data processing result",
|
||||
// )
|
||||
//
|
||||
// Use Cases:
|
||||
// - Debugging: Log both successful and failed Results in a pipeline
|
||||
// - Error tracking: Monitor error occurrences with custom log levels
|
||||
// - Custom logging: Use application-specific loggers and log levels
|
||||
// - Conditional logging: Enable/disable logging based on logger configuration
|
||||
func SLogWithCallback[A any](
|
||||
logLevel slog.Level,
|
||||
cb func(context.Context) *slog.Logger,
|
||||
message string) Kleisli[Result[A], A] {
|
||||
|
||||
return F.Pipe1(
|
||||
F.Flow2(
|
||||
// create the attribute to log depending on the condition
|
||||
result.ToSLogAttr[A](),
|
||||
// create an `IO` that logs the attribute
|
||||
curriedLog(logLevel, cb, message),
|
||||
),
|
||||
// preserve the original context
|
||||
reader.Chain(reader.Sequence(readerio.MapTo[struct{}, Result[A]])),
|
||||
)
|
||||
}
|
||||
|
||||
// SLog creates a Kleisli arrow that logs a Result value (success or error) with a message.
|
||||
//
|
||||
// This function logs both successful values and errors at Info level using the logger from the context.
|
||||
// It's a convenience wrapper around SLogWithCallback with standard settings.
|
||||
//
|
||||
// The logged output includes:
|
||||
// - For success: The message with the value as a structured "value" attribute
|
||||
// - For error: The message with the error as a structured "error" attribute
|
||||
//
|
||||
// The Result is passed through unchanged after logging, making this function transparent in the
|
||||
// computation pipeline.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type of the Result
|
||||
//
|
||||
// Parameters:
|
||||
// - message: A descriptive message to include in the log entry
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that logs the Result (value or error) and returns it unchanged
|
||||
//
|
||||
// Example with successful Result:
|
||||
//
|
||||
// pipeline := F.Pipe2(
|
||||
// fetchUser(123),
|
||||
// Chain(SLog[User]("Fetched user")),
|
||||
// Map(func(u User) string { return u.Name }),
|
||||
// )
|
||||
//
|
||||
// result := pipeline(context.Background())()
|
||||
// // If successful, logs: "Fetched user" value={ID:123 Name:"Alice"}
|
||||
// // If error, logs: "Fetched user" error="user not found"
|
||||
//
|
||||
// Example in error handling pipeline:
|
||||
//
|
||||
// pipeline := F.Pipe3(
|
||||
// fetchData(id),
|
||||
// Chain(SLog[Data]("Data fetched")),
|
||||
// Chain(validateData),
|
||||
// Chain(SLog[Data]("Data validated")),
|
||||
// Chain(processData),
|
||||
// )
|
||||
//
|
||||
// // Logs each step, including errors:
|
||||
// // "Data fetched" value={...} or error="..."
|
||||
// // "Data validated" value={...} or error="..."
|
||||
//
|
||||
// Use Cases:
|
||||
// - Debugging: Track both successful and failed Results in a pipeline
|
||||
// - Error monitoring: Log errors as they occur in the computation
|
||||
// - Flow tracking: See the progression of Results through a pipeline
|
||||
// - Troubleshooting: Identify where errors are introduced or propagated
|
||||
//
|
||||
// Note: This function logs the Result itself (which may contain an error), not just successful values.
|
||||
// For logging only successful values, use TapSLog instead.
|
||||
//
|
||||
//go:inline
|
||||
func SLog[A any](message string) Kleisli[Result[A], A] {
|
||||
return SLogWithCallback[A](slog.LevelInfo, logging.GetLoggerFromContext, message)
|
||||
}
|
||||
|
||||
// TapSLog creates an operator that logs only successful values with a message and passes them through unchanged.
|
||||
//
|
||||
// This function is useful for debugging and monitoring values as they flow through a ReaderIOResult
|
||||
// computation chain. Unlike SLog which logs both successes and errors, TapSLog only logs when the
|
||||
// computation is successful. If the computation contains an error, no logging occurs and the error
|
||||
// is propagated unchanged.
|
||||
//
|
||||
// The logged output includes:
|
||||
// - The provided message
|
||||
// - The value being passed through (as a structured "value" attribute)
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the value to log and pass through
|
||||
//
|
||||
// Parameters:
|
||||
// - message: A descriptive message to include in the log entry
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that logs successful values and returns them unchanged
|
||||
//
|
||||
// Example with simple value logging:
|
||||
//
|
||||
// fetchUser := func(id int) ReaderIOResult[User] {
|
||||
// return Of(User{ID: id, Name: "Alice"})
|
||||
// }
|
||||
//
|
||||
// pipeline := F.Pipe2(
|
||||
// fetchUser(123),
|
||||
// TapSLog[User]("Fetched user"),
|
||||
// Map(func(u User) string { return u.Name }),
|
||||
// )
|
||||
//
|
||||
// result := pipeline(context.Background())()
|
||||
// // Logs: "Fetched user" value={ID:123 Name:"Alice"}
|
||||
// // Returns: result.Of("Alice")
|
||||
//
|
||||
// Example in a processing pipeline:
|
||||
//
|
||||
// processOrder := F.Pipe4(
|
||||
// fetchOrder(orderId),
|
||||
// TapSLog[Order]("Order fetched"),
|
||||
// Chain(validateOrder),
|
||||
// TapSLog[Order]("Order validated"),
|
||||
// Chain(processPayment),
|
||||
// TapSLog[Payment]("Payment processed"),
|
||||
// )
|
||||
//
|
||||
// result := processOrder(context.Background())()
|
||||
// // Logs each successful step with the intermediate values
|
||||
// // If any step fails, subsequent TapSLog calls don't log
|
||||
//
|
||||
// Example with error handling:
|
||||
//
|
||||
// pipeline := F.Pipe3(
|
||||
// fetchData(id),
|
||||
// TapSLog[Data]("Data fetched"),
|
||||
// Chain(func(d Data) ReaderIOResult[Result] {
|
||||
// if d.IsValid() {
|
||||
// return Of(processData(d))
|
||||
// }
|
||||
// return Left[Result](errors.New("invalid data"))
|
||||
// }),
|
||||
// TapSLog[Result]("Data processed"),
|
||||
// )
|
||||
//
|
||||
// // If fetchData succeeds: logs "Data fetched" with the data
|
||||
// // If processing succeeds: logs "Data processed" with the result
|
||||
// // If processing fails: "Data processed" is NOT logged (error propagates)
|
||||
//
|
||||
// Use Cases:
|
||||
// - Debugging: Inspect intermediate successful values in a computation pipeline
|
||||
// - Monitoring: Track successful data flow through complex operations
|
||||
// - Troubleshooting: Identify where successful computations stop (last logged value before error)
|
||||
// - Auditing: Log important successful values for compliance or security
|
||||
// - Development: Understand data transformations during development
|
||||
//
|
||||
// Note: This function only logs successful values. Errors are silently propagated without logging.
|
||||
// For logging both successes and errors, use SLog instead.
|
||||
//
|
||||
//go:inline
|
||||
func TapSLog[A any](message string) Operator[A, A] {
|
||||
return readerio.ChainFirst(SLog[A](message))
|
||||
}
|
||||
662
v2/context/readerioresult/logging_test.go
Normal file
662
v2/context/readerioresult/logging_test.go
Normal file
@@ -0,0 +1,662 @@
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/logging"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestLoggingContext tests basic nested logging with correlation IDs
|
||||
func TestLoggingContext(t *testing.T) {
|
||||
data := F.Pipe2(
|
||||
Of("Sample"),
|
||||
LogEntryExit[string]("TestLoggingContext1"),
|
||||
LogEntryExit[string]("TestLoggingContext2"),
|
||||
)
|
||||
|
||||
assert.Equal(t, result.Of("Sample"), data(context.Background())())
|
||||
}
|
||||
|
||||
// TestLogEntryExitSuccess tests successful operation logging
|
||||
func TestLogEntryExitSuccess(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
operation := F.Pipe1(
|
||||
Of("success value"),
|
||||
LogEntryExit[string]("TestOperation"),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of("success value"), res)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "[entering]")
|
||||
assert.Contains(t, logOutput, "[exiting ]")
|
||||
assert.Contains(t, logOutput, "TestOperation")
|
||||
assert.Contains(t, logOutput, "ID=")
|
||||
assert.Contains(t, logOutput, "duration=")
|
||||
}
|
||||
|
||||
// TestLogEntryExitError tests error operation logging
|
||||
func TestLogEntryExitError(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
testErr := errors.New("test error")
|
||||
operation := F.Pipe1(
|
||||
Left[string](testErr),
|
||||
LogEntryExit[string]("FailingOperation"),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "[entering]")
|
||||
assert.Contains(t, logOutput, "[throwing]")
|
||||
assert.Contains(t, logOutput, "FailingOperation")
|
||||
assert.Contains(t, logOutput, "test error")
|
||||
assert.Contains(t, logOutput, "ID=")
|
||||
assert.Contains(t, logOutput, "duration=")
|
||||
}
|
||||
|
||||
// TestLogEntryExitNested tests nested operations with different IDs
|
||||
func TestLogEntryExitNested(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
innerOp := F.Pipe1(
|
||||
Of("inner"),
|
||||
LogEntryExit[string]("InnerOp"),
|
||||
)
|
||||
|
||||
outerOp := F.Pipe2(
|
||||
Of("outer"),
|
||||
LogEntryExit[string]("OuterOp"),
|
||||
Chain(func(s string) ReaderIOResult[string] {
|
||||
return innerOp
|
||||
}),
|
||||
)
|
||||
|
||||
res := outerOp(context.Background())()
|
||||
|
||||
assert.True(t, result.IsRight(res))
|
||||
|
||||
logOutput := buf.String()
|
||||
// Should have two different IDs
|
||||
assert.Contains(t, logOutput, "OuterOp")
|
||||
assert.Contains(t, logOutput, "InnerOp")
|
||||
|
||||
// Count entering and exiting logs
|
||||
enterCount := strings.Count(logOutput, "[entering]")
|
||||
exitCount := strings.Count(logOutput, "[exiting ]")
|
||||
assert.Equal(t, 2, enterCount, "Should have 2 entering logs")
|
||||
assert.Equal(t, 2, exitCount, "Should have 2 exiting logs")
|
||||
}
|
||||
|
||||
// TestLogEntryExitWithCallback tests custom log level and callback
|
||||
func TestLogEntryExitWithCallback(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelDebug,
|
||||
}))
|
||||
|
||||
customCallback := func(ctx context.Context) *slog.Logger {
|
||||
return logger
|
||||
}
|
||||
|
||||
operation := F.Pipe1(
|
||||
Of(42),
|
||||
LogEntryExitWithCallback[int](slog.LevelDebug, customCallback, "DebugOperation"),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "[entering]")
|
||||
assert.Contains(t, logOutput, "[exiting ]")
|
||||
assert.Contains(t, logOutput, "DebugOperation")
|
||||
assert.Contains(t, logOutput, "level=DEBUG")
|
||||
}
|
||||
|
||||
// TestLogEntryExitDisabled tests that logging can be disabled
|
||||
func TestLogEntryExitDisabled(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
// Create logger with level that disables info logs
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelError, // Only log errors
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
operation := F.Pipe1(
|
||||
Of("value"),
|
||||
LogEntryExit[string]("DisabledOperation"),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
|
||||
assert.True(t, result.IsRight(res))
|
||||
|
||||
// Should have no logs since level is ERROR
|
||||
logOutput := buf.String()
|
||||
assert.Empty(t, logOutput, "Should have no logs when logging is disabled")
|
||||
}
|
||||
|
||||
// TestLogEntryExitF tests custom entry/exit callbacks
|
||||
func TestLogEntryExitF(t *testing.T) {
|
||||
var entryCount, exitCount int
|
||||
|
||||
onEntry := func(ctx context.Context) IO[context.Context] {
|
||||
return func() context.Context {
|
||||
entryCount++
|
||||
return ctx
|
||||
}
|
||||
}
|
||||
|
||||
onExit := func(res Result[string]) ReaderIO[any] {
|
||||
return func(ctx context.Context) IO[any] {
|
||||
return func() any {
|
||||
exitCount++
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
operation := F.Pipe1(
|
||||
Of("test"),
|
||||
LogEntryExitF(onEntry, onExit),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
|
||||
assert.True(t, result.IsRight(res))
|
||||
assert.Equal(t, 1, entryCount, "Entry callback should be called once")
|
||||
assert.Equal(t, 1, exitCount, "Exit callback should be called once")
|
||||
}
|
||||
|
||||
// TestLogEntryExitFWithError tests custom callbacks with error
|
||||
func TestLogEntryExitFWithError(t *testing.T) {
|
||||
var entryCount, exitCount int
|
||||
var capturedError error
|
||||
|
||||
onEntry := func(ctx context.Context) IO[context.Context] {
|
||||
return func() context.Context {
|
||||
entryCount++
|
||||
return ctx
|
||||
}
|
||||
}
|
||||
|
||||
onExit := func(res Result[string]) ReaderIO[any] {
|
||||
return func(ctx context.Context) IO[any] {
|
||||
return func() any {
|
||||
exitCount++
|
||||
if result.IsLeft(res) {
|
||||
_, capturedError = result.Unwrap(res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
testErr := errors.New("custom error")
|
||||
operation := F.Pipe1(
|
||||
Left[string](testErr),
|
||||
LogEntryExitF(onEntry, onExit),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
assert.Equal(t, 1, entryCount, "Entry callback should be called once")
|
||||
assert.Equal(t, 1, exitCount, "Exit callback should be called once")
|
||||
assert.Equal(t, testErr, capturedError, "Should capture the error")
|
||||
}
|
||||
|
||||
// TestLoggingIDUniqueness tests that logging IDs are unique
|
||||
func TestLoggingIDUniqueness(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
// Run multiple operations
|
||||
for i := range 5 {
|
||||
op := F.Pipe1(
|
||||
Of(i),
|
||||
LogEntryExit[int]("Operation"),
|
||||
)
|
||||
op(context.Background())()
|
||||
}
|
||||
|
||||
logOutput := buf.String()
|
||||
|
||||
// Extract all IDs and verify they're unique
|
||||
lines := strings.Split(logOutput, "\n")
|
||||
ids := make(map[string]bool)
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "ID=") {
|
||||
// Extract ID value
|
||||
parts := strings.Split(line, "ID=")
|
||||
if len(parts) > 1 {
|
||||
idPart := strings.Fields(parts[1])[0]
|
||||
ids[idPart] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Should have 5 unique IDs (one per operation)
|
||||
assert.GreaterOrEqual(t, len(ids), 5, "Should have at least 5 unique IDs")
|
||||
}
|
||||
|
||||
// TestLogEntryExitWithContextLogger tests using logger from context
|
||||
func TestLogEntryExitWithContextLogger(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
contextLogger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
|
||||
ctx := logging.WithLogger(contextLogger)(context.Background())
|
||||
|
||||
operation := F.Pipe1(
|
||||
Of("context value"),
|
||||
LogEntryExit[string]("ContextOperation"),
|
||||
)
|
||||
|
||||
res := operation(ctx)()
|
||||
|
||||
assert.True(t, result.IsRight(res))
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "[entering]")
|
||||
assert.Contains(t, logOutput, "[exiting ]")
|
||||
assert.Contains(t, logOutput, "ContextOperation")
|
||||
}
|
||||
|
||||
// TestLogEntryExitTiming tests that duration is captured
|
||||
func TestLogEntryExitTiming(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
// Operation with delay
|
||||
slowOp := func(ctx context.Context) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
return result.Of("done")
|
||||
}
|
||||
}
|
||||
|
||||
operation := F.Pipe1(
|
||||
slowOp,
|
||||
LogEntryExit[string]("SlowOperation"),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
|
||||
assert.True(t, result.IsRight(res))
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "duration=")
|
||||
|
||||
// Verify duration is present in exit log
|
||||
lines := strings.Split(logOutput, "\n")
|
||||
var foundDuration bool
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "[exiting ]") && strings.Contains(line, "duration=") {
|
||||
foundDuration = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, foundDuration, "Exit log should contain duration")
|
||||
}
|
||||
|
||||
// TestLogEntryExitChainedOperations tests complex chained operations
|
||||
func TestLogEntryExitChainedOperations(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
step1 := F.Pipe1(
|
||||
Of(1),
|
||||
LogEntryExit[int]("Step1"),
|
||||
)
|
||||
|
||||
step2 := F.Flow3(
|
||||
N.Mul(2),
|
||||
Of,
|
||||
LogEntryExit[int]("Step2"),
|
||||
)
|
||||
|
||||
step3 := F.Flow3(
|
||||
strconv.Itoa,
|
||||
Of,
|
||||
LogEntryExit[string]("Step3"),
|
||||
)
|
||||
|
||||
pipeline := F.Pipe1(
|
||||
step1,
|
||||
Chain(F.Flow2(
|
||||
step2,
|
||||
Chain(step3),
|
||||
)),
|
||||
)
|
||||
|
||||
res := pipeline(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of("2"), res)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Step1")
|
||||
assert.Contains(t, logOutput, "Step2")
|
||||
assert.Contains(t, logOutput, "Step3")
|
||||
|
||||
// Verify all steps completed
|
||||
assert.Equal(t, 3, strings.Count(logOutput, "[entering]"))
|
||||
assert.Equal(t, 3, strings.Count(logOutput, "[exiting ]"))
|
||||
}
|
||||
|
||||
// TestTapSLog tests basic TapSLog functionality
|
||||
func TestTapSLog(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
operation := F.Pipe2(
|
||||
Of(42),
|
||||
TapSLog[int]("Processing value"),
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(84), res)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Processing value")
|
||||
assert.Contains(t, logOutput, "value=42")
|
||||
}
|
||||
|
||||
// TestTapSLogInPipeline tests TapSLog in a multi-step pipeline
|
||||
func TestTapSLogInPipeline(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
step1 := F.Pipe2(
|
||||
Of("hello"),
|
||||
TapSLog[string]("Step 1: Initial value"),
|
||||
Map(func(s string) string { return s + " world" }),
|
||||
)
|
||||
|
||||
step2 := F.Pipe2(
|
||||
step1,
|
||||
TapSLog[string]("Step 2: After concatenation"),
|
||||
Map(S.Size),
|
||||
)
|
||||
|
||||
pipeline := F.Pipe1(
|
||||
step2,
|
||||
TapSLog[int]("Step 3: Final length"),
|
||||
)
|
||||
|
||||
res := pipeline(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(11), res)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Step 1: Initial value")
|
||||
assert.Contains(t, logOutput, "value=hello")
|
||||
assert.Contains(t, logOutput, "Step 2: After concatenation")
|
||||
assert.Contains(t, logOutput, `value="hello world"`)
|
||||
assert.Contains(t, logOutput, "Step 3: Final length")
|
||||
assert.Contains(t, logOutput, "value=11")
|
||||
}
|
||||
|
||||
// TestTapSLogWithError tests that TapSLog logs errors (via SLog)
|
||||
func TestTapSLogWithError(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
testErr := errors.New("computation failed")
|
||||
pipeline := F.Pipe2(
|
||||
Left[int](testErr),
|
||||
TapSLog[int]("Error logged"),
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
|
||||
res := pipeline(context.Background())()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
|
||||
logOutput := buf.String()
|
||||
// TapSLog uses SLog internally, which logs both successes and errors
|
||||
assert.Contains(t, logOutput, "Error logged")
|
||||
assert.Contains(t, logOutput, "error")
|
||||
assert.Contains(t, logOutput, "computation failed")
|
||||
}
|
||||
|
||||
// TestTapSLogWithStruct tests TapSLog with structured data
|
||||
func TestTapSLogWithStruct(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
user := User{ID: 123, Name: "Alice"}
|
||||
operation := F.Pipe2(
|
||||
Of(user),
|
||||
TapSLog[User]("User data"),
|
||||
Map(func(u User) string { return u.Name }),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of("Alice"), res)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "User data")
|
||||
assert.Contains(t, logOutput, "ID:123")
|
||||
assert.Contains(t, logOutput, "Name:Alice")
|
||||
}
|
||||
|
||||
// TestTapSLogDisabled tests that TapSLog respects logger level
|
||||
func TestTapSLogDisabled(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
// Create logger with level that disables info logs
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelError, // Only log errors
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
operation := F.Pipe2(
|
||||
Of(42),
|
||||
TapSLog[int]("This should not be logged"),
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(84), res)
|
||||
|
||||
// Should have no logs since level is ERROR
|
||||
logOutput := buf.String()
|
||||
assert.Empty(t, logOutput, "Should have no logs when logging is disabled")
|
||||
}
|
||||
|
||||
// TestTapSLogWithContextLogger tests TapSLog using logger from context
|
||||
func TestTapSLogWithContextLogger(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
contextLogger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
|
||||
ctx := logging.WithLogger(contextLogger)(context.Background())
|
||||
|
||||
operation := F.Pipe2(
|
||||
Of("test value"),
|
||||
TapSLog[string]("Context logger test"),
|
||||
Map(S.Size),
|
||||
)
|
||||
|
||||
res := operation(ctx)()
|
||||
|
||||
assert.Equal(t, result.Of(10), res)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Context logger test")
|
||||
assert.Contains(t, logOutput, `value="test value"`)
|
||||
}
|
||||
|
||||
// TestSLogLogsSuccessValue tests that SLog logs successful Result values
|
||||
func TestSLogLogsSuccessValue(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a Result and log it
|
||||
res1 := result.Of(42)
|
||||
logged := SLog[int]("Result value")(res1)(ctx)()
|
||||
|
||||
assert.Equal(t, result.Of(42), logged)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Result value")
|
||||
assert.Contains(t, logOutput, "value=42")
|
||||
}
|
||||
|
||||
// TestSLogLogsErrorValue tests that SLog logs error Result values
|
||||
func TestSLogLogsErrorValue(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
testErr := errors.New("test error")
|
||||
|
||||
// Create an error Result and log it
|
||||
res1 := result.Left[int](testErr)
|
||||
logged := SLog[int]("Result value")(res1)(ctx)()
|
||||
|
||||
assert.True(t, result.IsLeft(logged))
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Result value")
|
||||
assert.Contains(t, logOutput, "error")
|
||||
assert.Contains(t, logOutput, "test error")
|
||||
}
|
||||
|
||||
// TestSLogWithCallbackCustomLevel tests SLogWithCallback with custom log level
|
||||
func TestSLogWithCallbackCustomLevel(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelDebug,
|
||||
}))
|
||||
|
||||
customCallback := func(ctx context.Context) *slog.Logger {
|
||||
return logger
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a Result and log it with custom callback
|
||||
res1 := result.Of(42)
|
||||
logged := SLogWithCallback[int](slog.LevelDebug, customCallback, "Debug result")(res1)(ctx)()
|
||||
|
||||
assert.Equal(t, result.Of(42), logged)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Debug result")
|
||||
assert.Contains(t, logOutput, "value=42")
|
||||
assert.Contains(t, logOutput, "level=DEBUG")
|
||||
}
|
||||
|
||||
// TestSLogWithCallbackLogsError tests SLogWithCallback logs errors
|
||||
func TestSLogWithCallbackLogsError(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelWarn,
|
||||
}))
|
||||
|
||||
customCallback := func(ctx context.Context) *slog.Logger {
|
||||
return logger
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
testErr := errors.New("warning error")
|
||||
|
||||
// Create an error Result and log it with custom callback
|
||||
res1 := result.Left[int](testErr)
|
||||
logged := SLogWithCallback[int](slog.LevelWarn, customCallback, "Warning result")(res1)(ctx)()
|
||||
|
||||
assert.True(t, result.IsLeft(logged))
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Warning result")
|
||||
assert.Contains(t, logOutput, "error")
|
||||
assert.Contains(t, logOutput, "warning error")
|
||||
assert.Contains(t, logOutput, "level=WARN")
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/readerio"
|
||||
"github.com/IBM/fp-go/v2/context/readerresult"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/errors"
|
||||
@@ -26,10 +27,11 @@ import (
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"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 (
|
||||
@@ -150,7 +152,7 @@ func MapTo[A, B any](b B) Operator[A, B] {
|
||||
//
|
||||
//go:inline
|
||||
func MonadChain[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[B] {
|
||||
return RIOR.MonadChain(ma, f)
|
||||
return RIOR.MonadChain(ma, function.Flow2(f, WithContext))
|
||||
}
|
||||
|
||||
// Chain sequences two [ReaderIOResult] computations, where the second depends on the result of the first.
|
||||
@@ -163,7 +165,7 @@ func MonadChain[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[
|
||||
//
|
||||
//go:inline
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||
return RIOR.Chain(f)
|
||||
return RIOR.Chain(function.Flow2(f, WithContext))
|
||||
}
|
||||
|
||||
// MonadChainFirst sequences two [ReaderIOResult] computations but returns the result of the first.
|
||||
@@ -177,12 +179,12 @@ func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirst[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A] {
|
||||
return RIOR.MonadChainFirst(ma, f)
|
||||
return RIOR.MonadChainFirst(ma, function.Flow2(f, WithContext))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadTap[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A] {
|
||||
return RIOR.MonadTap(ma, f)
|
||||
return RIOR.MonadTap(ma, function.Flow2(f, WithContext))
|
||||
}
|
||||
|
||||
// ChainFirst sequences two [ReaderIOResult] computations but returns the result of the first.
|
||||
@@ -195,12 +197,12 @@ func MonadTap[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A]
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
||||
return RIOR.ChainFirst(f)
|
||||
return RIOR.ChainFirst(function.Flow2(f, WithContext))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Tap[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
||||
return RIOR.Tap(f)
|
||||
return RIOR.Tap(function.Flow2(f, WithContext))
|
||||
}
|
||||
|
||||
// Of creates a [ReaderIOResult] that always succeeds with the given value.
|
||||
@@ -243,14 +245,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
|
||||
@@ -382,7 +384,7 @@ func Ask() ReaderIOResult[context.Context] {
|
||||
// Returns a new ReaderIOResult with the chained computation.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B]) ReaderIOResult[B] {
|
||||
func MonadChainEitherK[A, B any](ma ReaderIOResult[A], f either.Kleisli[error, A, B]) ReaderIOResult[B] {
|
||||
return RIOR.MonadChainEitherK(ma, f)
|
||||
}
|
||||
|
||||
@@ -395,7 +397,7 @@ func MonadChainEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B]) Read
|
||||
// Returns a function that chains the Either-returning function.
|
||||
//
|
||||
//go:inline
|
||||
func ChainEitherK[A, B any](f func(A) Either[B]) Operator[A, B] {
|
||||
func ChainEitherK[A, B any](f either.Kleisli[error, A, B]) Operator[A, B] {
|
||||
return RIOR.ChainEitherK[context.Context](f)
|
||||
}
|
||||
|
||||
@@ -409,12 +411,12 @@ func ChainEitherK[A, B any](f func(A) Either[B]) Operator[A, B] {
|
||||
// Returns a ReaderIOResult with the original value if both computations succeed.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B]) ReaderIOResult[A] {
|
||||
func MonadChainFirstEitherK[A, B any](ma ReaderIOResult[A], f either.Kleisli[error, A, B]) ReaderIOResult[A] {
|
||||
return RIOR.MonadChainFirstEitherK(ma, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadTapEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B]) ReaderIOResult[A] {
|
||||
func MonadTapEitherK[A, B any](ma ReaderIOResult[A], f either.Kleisli[error, A, B]) ReaderIOResult[A] {
|
||||
return RIOR.MonadTapEitherK(ma, f)
|
||||
}
|
||||
|
||||
@@ -427,12 +429,12 @@ func MonadTapEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B]) Reader
|
||||
// Returns a function that chains the Either-returning function.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstEitherK[A, B any](f func(A) Either[B]) Operator[A, A] {
|
||||
func ChainFirstEitherK[A, B any](f either.Kleisli[error, A, B]) Operator[A, A] {
|
||||
return RIOR.ChainFirstEitherK[context.Context](f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TapEitherK[A, B any](f func(A) Either[B]) Operator[A, A] {
|
||||
func TapEitherK[A, B any](f either.Kleisli[error, A, B]) Operator[A, A] {
|
||||
return RIOR.TapEitherK[context.Context](f)
|
||||
}
|
||||
|
||||
@@ -445,7 +447,7 @@ func TapEitherK[A, B any](f func(A) Either[B]) Operator[A, A] {
|
||||
// Returns a function that chains Option-returning functions into ReaderIOResult.
|
||||
//
|
||||
//go:inline
|
||||
func ChainOptionK[A, B any](onNone func() error) func(func(A) Option[B]) Operator[A, B] {
|
||||
func ChainOptionK[A, B any](onNone func() error) func(option.Kleisli[A, B]) Operator[A, B] {
|
||||
return RIOR.ChainOptionK[context.Context, A, B](onNone)
|
||||
}
|
||||
|
||||
@@ -527,7 +529,7 @@ func Never[A any]() ReaderIOResult[A] {
|
||||
// Returns a new ReaderIOResult with the chained IO computation.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderIOResult[B] {
|
||||
func MonadChainIOK[A, B any](ma ReaderIOResult[A], f io.Kleisli[A, B]) ReaderIOResult[B] {
|
||||
return RIOR.MonadChainIOK(ma, f)
|
||||
}
|
||||
|
||||
@@ -540,7 +542,7 @@ func MonadChainIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderIOResu
|
||||
// Returns a function that chains the IO-returning function.
|
||||
//
|
||||
//go:inline
|
||||
func ChainIOK[A, B any](f func(A) IO[B]) Operator[A, B] {
|
||||
func ChainIOK[A, B any](f io.Kleisli[A, B]) Operator[A, B] {
|
||||
return RIOR.ChainIOK[context.Context](f)
|
||||
}
|
||||
|
||||
@@ -554,12 +556,12 @@ func ChainIOK[A, B any](f func(A) IO[B]) Operator[A, B] {
|
||||
// Returns a ReaderIOResult with the original value after executing the IO.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderIOResult[A] {
|
||||
func MonadChainFirstIOK[A, B any](ma ReaderIOResult[A], f io.Kleisli[A, B]) ReaderIOResult[A] {
|
||||
return RIOR.MonadChainFirstIOK(ma, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadTapIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderIOResult[A] {
|
||||
func MonadTapIOK[A, B any](ma ReaderIOResult[A], f io.Kleisli[A, B]) ReaderIOResult[A] {
|
||||
return RIOR.MonadTapIOK(ma, f)
|
||||
}
|
||||
|
||||
@@ -572,12 +574,12 @@ func MonadTapIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderIOResult
|
||||
// Returns a function that chains the IO-returning function.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
|
||||
func ChainFirstIOK[A, B any](f io.Kleisli[A, B]) Operator[A, A] {
|
||||
return RIOR.ChainFirstIOK[context.Context](f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TapIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
|
||||
func TapIOK[A, B any](f io.Kleisli[A, B]) Operator[A, A] {
|
||||
return RIOR.TapIOK[context.Context](f)
|
||||
}
|
||||
|
||||
@@ -590,7 +592,7 @@ func TapIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
|
||||
// Returns a function that chains the IOResult-returning function.
|
||||
//
|
||||
//go:inline
|
||||
func ChainIOEitherK[A, B any](f func(A) IOResult[B]) Operator[A, B] {
|
||||
func ChainIOEitherK[A, B any](f ioresult.Kleisli[A, B]) Operator[A, B] {
|
||||
return RIOR.ChainIOEitherK[context.Context](f)
|
||||
}
|
||||
|
||||
@@ -753,7 +755,7 @@ func Flap[B, A any](a A) Operator[func(A) B, B] {
|
||||
//
|
||||
//go:inline
|
||||
func Fold[A, B any](onLeft Kleisli[error, B], onRight Kleisli[A, B]) Operator[A, B] {
|
||||
return RIOR.Fold(onLeft, onRight)
|
||||
return RIOR.Fold(function.Flow2(onLeft, WithContext), function.Flow2(onRight, WithContext))
|
||||
}
|
||||
|
||||
// GetOrElse extracts the value from a [ReaderIOResult], providing a default via a function if it fails.
|
||||
@@ -765,7 +767,7 @@ func Fold[A, B any](onLeft Kleisli[error, B], onRight Kleisli[A, B]) Operator[A,
|
||||
// Returns a function that converts a ReaderIOResult to a ReaderIO.
|
||||
//
|
||||
//go:inline
|
||||
func GetOrElse[A any](onLeft func(error) ReaderIO[A]) func(ReaderIOResult[A]) ReaderIO[A] {
|
||||
func GetOrElse[A any](onLeft readerio.Kleisli[error, A]) func(ReaderIOResult[A]) ReaderIO[A] {
|
||||
return RIOR.GetOrElse(onLeft)
|
||||
}
|
||||
|
||||
@@ -858,32 +860,32 @@ func TapReaderResultK[A, B any](f readerresult.Kleisli[A, B]) Operator[A, A] {
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadChainReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[context.Context, A, B]) ReaderIOResult[B] {
|
||||
func MonadChainReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[A, B]) ReaderIOResult[B] {
|
||||
return RIOR.MonadChainReaderIOK(ma, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainReaderIOK[A, B any](f readerio.Kleisli[context.Context, A, B]) Operator[A, B] {
|
||||
func ChainReaderIOK[A, B any](f readerio.Kleisli[A, B]) Operator[A, B] {
|
||||
return RIOR.ChainReaderIOK(f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadChainFirstReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[context.Context, A, B]) ReaderIOResult[A] {
|
||||
func MonadChainFirstReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[A, B]) ReaderIOResult[A] {
|
||||
return RIOR.MonadChainFirstReaderIOK(ma, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadTapReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[context.Context, A, B]) ReaderIOResult[A] {
|
||||
func MonadTapReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[A, B]) ReaderIOResult[A] {
|
||||
return RIOR.MonadTapReaderIOK(ma, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainFirstReaderIOK[A, B any](f readerio.Kleisli[context.Context, A, B]) Operator[A, A] {
|
||||
func ChainFirstReaderIOK[A, B any](f readerio.Kleisli[A, B]) Operator[A, A] {
|
||||
return RIOR.ChainFirstReaderIOK(f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TapReaderIOK[A, B any](f readerio.Kleisli[context.Context, A, B]) Operator[A, A] {
|
||||
func TapReaderIOK[A, B any](f readerio.Kleisli[A, B]) Operator[A, A] {
|
||||
return RIOR.TapReaderIOK(f)
|
||||
}
|
||||
|
||||
@@ -913,15 +915,15 @@ func Read[A any](r context.Context) func(ReaderIOResult[A]) IOResult[A] {
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainLeft[A any](fa ReaderIOResult[A], f Kleisli[error, A]) ReaderIOResult[A] {
|
||||
return RIOR.MonadChainLeft(fa, f)
|
||||
return RIOR.MonadChainLeft(fa, function.Flow2(f, WithContext))
|
||||
}
|
||||
|
||||
// ChainLeft is the curried version of [MonadChainLeft].
|
||||
// It returns a function that chains a computation on the left (error) side of a [ReaderIOResult].
|
||||
//
|
||||
//go:inline
|
||||
func ChainLeft[A any](f Kleisli[error, A]) func(ReaderIOResult[A]) ReaderIOResult[A] {
|
||||
return RIOR.ChainLeft(f)
|
||||
func ChainLeft[A any](f Kleisli[error, A]) Operator[A, A] {
|
||||
return RIOR.ChainLeft(function.Flow2(f, WithContext))
|
||||
}
|
||||
|
||||
// MonadChainFirstLeft chains a computation on the left (error) side but always returns the original error.
|
||||
@@ -934,12 +936,12 @@ func ChainLeft[A any](f Kleisli[error, A]) func(ReaderIOResult[A]) ReaderIOResul
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOResult[A] {
|
||||
return RIOR.MonadChainFirstLeft(ma, f)
|
||||
return RIOR.MonadChainFirstLeft(ma, function.Flow2(f, WithContext))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadTapLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOResult[A] {
|
||||
return RIOR.MonadTapLeft(ma, f)
|
||||
return RIOR.MonadTapLeft(ma, function.Flow2(f, WithContext))
|
||||
}
|
||||
|
||||
// ChainFirstLeft is the curried version of [MonadChainFirstLeft].
|
||||
@@ -951,10 +953,212 @@ func MonadTapLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOR
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
|
||||
return RIOR.ChainFirstLeft[A](f)
|
||||
return RIOR.ChainFirstLeft[A](function.Flow2(f, WithContext))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TapLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
|
||||
return RIOR.TapLeft[A](f)
|
||||
return RIOR.TapLeft[A](function.Flow2(f, WithContext))
|
||||
}
|
||||
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -567,15 +567,13 @@ func TestMemoize(t *testing.T) {
|
||||
res1 := computation(context.Background())()
|
||||
assert.True(t, E.IsRight(res1))
|
||||
val1 := E.ToOption(res1)
|
||||
v1, _ := O.Unwrap(val1)
|
||||
assert.Equal(t, 1, v1)
|
||||
assert.Equal(t, O.Of(1), val1)
|
||||
|
||||
// Second execution should return cached value
|
||||
res2 := computation(context.Background())()
|
||||
assert.True(t, E.IsRight(res2))
|
||||
val2 := E.ToOption(res2)
|
||||
v2, _ := O.Unwrap(val2)
|
||||
assert.Equal(t, 1, v2)
|
||||
assert.Equal(t, O.Of(1), val2)
|
||||
|
||||
// Counter should only be incremented once
|
||||
assert.Equal(t, 1, counter)
|
||||
@@ -739,9 +737,7 @@ func TestTraverseArray(t *testing.T) {
|
||||
res := result(context.Background())()
|
||||
assert.True(t, E.IsRight(res))
|
||||
arrOpt := E.ToOption(res)
|
||||
assert.True(t, O.IsSome(arrOpt))
|
||||
resultArr, _ := O.Unwrap(arrOpt)
|
||||
assert.Equal(t, []int{2, 4, 6}, resultArr)
|
||||
assert.Equal(t, O.Of([]int{2, 4, 6}), arrOpt)
|
||||
})
|
||||
|
||||
t.Run("TraverseArray with error", func(t *testing.T) {
|
||||
@@ -765,9 +761,7 @@ func TestSequenceArray(t *testing.T) {
|
||||
res := result(context.Background())()
|
||||
assert.True(t, E.IsRight(res))
|
||||
arrOpt := E.ToOption(res)
|
||||
assert.True(t, O.IsSome(arrOpt))
|
||||
resultArr, _ := O.Unwrap(arrOpt)
|
||||
assert.Equal(t, []int{1, 2, 3}, resultArr)
|
||||
assert.Equal(t, O.Of([]int{1, 2, 3}), arrOpt)
|
||||
}
|
||||
|
||||
func TestTraverseRecord(t *testing.T) {
|
||||
|
||||
184
v2/context/readerioresult/rec.go
Normal file
184
v2/context/readerioresult/rec.go
Normal file
@@ -0,0 +1,184 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||
)
|
||||
|
||||
// TailRec implements stack-safe tail recursion for the context-aware ReaderIOResult monad.
|
||||
//
|
||||
// This function enables recursive computations that combine four powerful concepts:
|
||||
// - Context awareness: Automatic cancellation checking via [context.Context]
|
||||
// - Environment dependency (Reader aspect): Access to configuration, context, or dependencies
|
||||
// - Side effects (IO aspect): Logging, file I/O, network calls, etc.
|
||||
// - Error handling (Either aspect): Computations that can fail with an error
|
||||
//
|
||||
// The function uses an iterative loop to execute the recursion, making it safe for deep
|
||||
// or unbounded recursion without risking stack overflow. Additionally, it integrates
|
||||
// context cancellation checking through [WithContext], ensuring that recursive computations
|
||||
// can be cancelled gracefully.
|
||||
//
|
||||
// # How It Works
|
||||
//
|
||||
// TailRec takes a Kleisli arrow that returns Either[A, B]:
|
||||
// - Left(A): Continue recursion with the new state A
|
||||
// - Right(B): Terminate recursion successfully and return the final result B
|
||||
//
|
||||
// The function wraps each iteration with [WithContext] to ensure context cancellation
|
||||
// is checked before each recursive step. If the context is cancelled, the recursion
|
||||
// terminates early with a context cancellation error.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The state type that changes during recursion
|
||||
// - B: The final result type when recursion terminates successfully
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A Kleisli arrow (A => ReaderIOResult[Either[A, B]]) that:
|
||||
// - Takes the current state A
|
||||
// - Returns a ReaderIOResult that depends on [context.Context]
|
||||
// - Can fail with error (Left in the outer Either)
|
||||
// - Produces Either[A, B] to control recursion flow (Right in the outer Either)
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A Kleisli arrow (A => ReaderIOResult[B]) that:
|
||||
// - Takes an initial state A
|
||||
// - Returns a ReaderIOResult that requires [context.Context]
|
||||
// - Can fail with error or context cancellation
|
||||
// - Produces the final result B after recursion completes
|
||||
//
|
||||
// # Context Cancellation
|
||||
//
|
||||
// Unlike the base [readerioresult.TailRec], this version automatically integrates
|
||||
// context cancellation checking:
|
||||
// - Each recursive iteration checks if the context is cancelled
|
||||
// - If cancelled, recursion terminates immediately with a cancellation error
|
||||
// - This prevents runaway recursive computations in cancelled contexts
|
||||
// - Enables responsive cancellation for long-running recursive operations
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// 1. Cancellable recursive algorithms:
|
||||
// - Tree traversals that can be cancelled mid-operation
|
||||
// - Graph algorithms with timeout requirements
|
||||
// - Recursive parsers that respect cancellation
|
||||
//
|
||||
// 2. Long-running recursive computations:
|
||||
// - File system traversals with cancellation support
|
||||
// - Network operations with timeout handling
|
||||
// - Database operations with connection timeout awareness
|
||||
//
|
||||
// 3. Interactive recursive operations:
|
||||
// - User-initiated operations that can be cancelled
|
||||
// - Background tasks with cancellation support
|
||||
// - Streaming operations with graceful shutdown
|
||||
//
|
||||
// # Example: Cancellable Countdown
|
||||
//
|
||||
// countdownStep := func(n int) readerioresult.ReaderIOResult[either.Either[int, string]] {
|
||||
// return func(ctx context.Context) ioeither.IOEither[error, either.Either[int, string]] {
|
||||
// return func() either.Either[error, either.Either[int, string]] {
|
||||
// if n <= 0 {
|
||||
// return either.Right[error](either.Right[int]("Done!"))
|
||||
// }
|
||||
// // Simulate some work
|
||||
// time.Sleep(100 * time.Millisecond)
|
||||
// return either.Right[error](either.Left[string](n - 1))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// countdown := readerioresult.TailRec(countdownStep)
|
||||
//
|
||||
// // With cancellation
|
||||
// ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
|
||||
// defer cancel()
|
||||
// result := countdown(10)(ctx)() // Will be cancelled after ~500ms
|
||||
//
|
||||
// # Example: Cancellable File Processing
|
||||
//
|
||||
// type ProcessState struct {
|
||||
// files []string
|
||||
// processed []string
|
||||
// }
|
||||
//
|
||||
// processStep := func(state ProcessState) readerioresult.ReaderIOResult[either.Either[ProcessState, []string]] {
|
||||
// return func(ctx context.Context) ioeither.IOEither[error, either.Either[ProcessState, []string]] {
|
||||
// return func() either.Either[error, either.Either[ProcessState, []string]] {
|
||||
// if len(state.files) == 0 {
|
||||
// return either.Right[error](either.Right[ProcessState](state.processed))
|
||||
// }
|
||||
//
|
||||
// file := state.files[0]
|
||||
// // Process file (this could be cancelled via context)
|
||||
// if err := processFileWithContext(ctx, file); err != nil {
|
||||
// return either.Left[either.Either[ProcessState, []string]](err)
|
||||
// }
|
||||
//
|
||||
// return either.Right[error](either.Left[[]string](ProcessState{
|
||||
// files: state.files[1:],
|
||||
// processed: append(state.processed, file),
|
||||
// }))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// processFiles := readerioresult.TailRec(processStep)
|
||||
// ctx, cancel := context.WithCancel(context.Background())
|
||||
//
|
||||
// // Can be cancelled at any point during processing
|
||||
// go func() {
|
||||
// time.Sleep(2 * time.Second)
|
||||
// cancel() // Cancel after 2 seconds
|
||||
// }()
|
||||
//
|
||||
// result := processFiles(ProcessState{files: manyFiles})(ctx)()
|
||||
//
|
||||
// # Stack Safety
|
||||
//
|
||||
// The iterative implementation ensures that even deeply recursive computations
|
||||
// (thousands or millions of iterations) will not cause stack overflow, while
|
||||
// still respecting context cancellation:
|
||||
//
|
||||
// // Safe for very large inputs with cancellation support
|
||||
// largeCountdown := readerioresult.TailRec(countdownStep)
|
||||
// ctx := context.Background()
|
||||
// result := largeCountdown(1000000)(ctx)() // Safe, no stack overflow
|
||||
//
|
||||
// # Performance Considerations
|
||||
//
|
||||
// - Each iteration includes context cancellation checking overhead
|
||||
// - Context checking happens before each recursive step
|
||||
// - For performance-critical code, consider the cancellation checking cost
|
||||
// - The [WithContext] wrapper adds minimal overhead for cancellation safety
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - [readerioresult.TailRec]: Base tail recursion without automatic context checking
|
||||
// - [WithContext]: Context cancellation wrapper used internally
|
||||
// - [Chain]: For sequencing ReaderIOResult computations
|
||||
// - [Ask]: For accessing the context
|
||||
// - [Left]/[Right]: For creating error/success values
|
||||
//
|
||||
//go:inline
|
||||
func TailRec[A, B any](f Kleisli[A, either.Either[A, B]]) Kleisli[A, B] {
|
||||
return RIOR.TailRec(F.Flow2(f, WithContext))
|
||||
}
|
||||
434
v2/context/readerioresult/rec_test.go
Normal file
434
v2/context/readerioresult/rec_test.go
Normal file
@@ -0,0 +1,434 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTailRec_BasicRecursion(t *testing.T) {
|
||||
// Test basic countdown recursion
|
||||
countdownStep := func(n int) ReaderIOResult[E.Either[int, string]] {
|
||||
return func(ctx context.Context) IOEither[E.Either[int, string]] {
|
||||
return func() Either[E.Either[int, string]] {
|
||||
if n <= 0 {
|
||||
return E.Right[error](E.Right[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](E.Left[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(5)(context.Background())()
|
||||
|
||||
assert.Equal(t, E.Of[error]("Done!"), result)
|
||||
}
|
||||
|
||||
func TestTailRec_FactorialRecursion(t *testing.T) {
|
||||
// Test factorial computation using tail recursion
|
||||
type FactorialState struct {
|
||||
n int
|
||||
acc int
|
||||
}
|
||||
|
||||
factorialStep := func(state FactorialState) ReaderIOResult[E.Either[FactorialState, int]] {
|
||||
return func(ctx context.Context) IOEither[E.Either[FactorialState, int]] {
|
||||
return func() Either[E.Either[FactorialState, int]] {
|
||||
if state.n <= 1 {
|
||||
return E.Right[error](E.Right[FactorialState](state.acc))
|
||||
}
|
||||
return E.Right[error](E.Left[int](FactorialState{
|
||||
n: state.n - 1,
|
||||
acc: state.acc * state.n,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
factorial := TailRec(factorialStep)
|
||||
result := factorial(FactorialState{n: 5, acc: 1})(context.Background())()
|
||||
|
||||
assert.Equal(t, E.Of[error](120), result) // 5! = 120
|
||||
}
|
||||
|
||||
func TestTailRec_ErrorHandling(t *testing.T) {
|
||||
// Test that errors are properly propagated
|
||||
testErr := errors.New("computation error")
|
||||
|
||||
errorStep := func(n int) ReaderIOResult[E.Either[int, string]] {
|
||||
return func(ctx context.Context) IOEither[E.Either[int, string]] {
|
||||
return func() Either[E.Either[int, string]] {
|
||||
if n == 3 {
|
||||
return E.Left[E.Either[int, string]](testErr)
|
||||
}
|
||||
if n <= 0 {
|
||||
return E.Right[error](E.Right[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](E.Left[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
errorRecursion := TailRec(errorStep)
|
||||
result := errorRecursion(5)(context.Background())()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
err := E.ToError(result)
|
||||
assert.Equal(t, testErr, err)
|
||||
}
|
||||
|
||||
func TestTailRec_ContextCancellation(t *testing.T) {
|
||||
// Test that recursion gets cancelled early when context is canceled
|
||||
var iterationCount int32
|
||||
|
||||
slowStep := func(n int) ReaderIOResult[E.Either[int, string]] {
|
||||
return func(ctx context.Context) IOEither[E.Either[int, string]] {
|
||||
return func() Either[E.Either[int, string]] {
|
||||
atomic.AddInt32(&iterationCount, 1)
|
||||
|
||||
// Simulate some work
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
if n <= 0 {
|
||||
return E.Right[error](E.Right[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](E.Left[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slowRecursion := TailRec(slowStep)
|
||||
|
||||
// Create a context that will be cancelled after 100ms
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
start := time.Now()
|
||||
result := slowRecursion(10)(ctx)()
|
||||
elapsed := time.Since(start)
|
||||
|
||||
// Should be cancelled and return an error
|
||||
assert.True(t, E.IsLeft(result))
|
||||
|
||||
// Should complete quickly due to cancellation (much less than 10 * 50ms = 500ms)
|
||||
assert.Less(t, elapsed, 200*time.Millisecond)
|
||||
|
||||
// Should have executed only a few iterations before cancellation
|
||||
iterations := atomic.LoadInt32(&iterationCount)
|
||||
assert.Less(t, iterations, int32(5), "Should have been cancelled before completing all iterations")
|
||||
}
|
||||
|
||||
func TestTailRec_ImmediateCancellation(t *testing.T) {
|
||||
// Test with an already cancelled context
|
||||
countdownStep := func(n int) ReaderIOResult[E.Either[int, string]] {
|
||||
return func(ctx context.Context) IOEither[E.Either[int, string]] {
|
||||
return func() Either[E.Either[int, string]] {
|
||||
if n <= 0 {
|
||||
return E.Right[error](E.Right[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](E.Left[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
|
||||
// Create an already cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
result := countdown(5)(ctx)()
|
||||
|
||||
// Should immediately return a cancellation error
|
||||
assert.True(t, E.IsLeft(result))
|
||||
err := E.ToError(result)
|
||||
assert.Equal(t, context.Canceled, err)
|
||||
}
|
||||
|
||||
func TestTailRec_StackSafety(t *testing.T) {
|
||||
// Test that deep recursion doesn't cause stack overflow
|
||||
const largeN = 10000
|
||||
|
||||
countdownStep := func(n int) ReaderIOResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) IOEither[E.Either[int, int]] {
|
||||
return func() Either[E.Either[int, int]] {
|
||||
if n <= 0 {
|
||||
return E.Right[error](E.Right[int](0))
|
||||
}
|
||||
return E.Right[error](E.Left[int](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(largeN)(context.Background())()
|
||||
|
||||
assert.Equal(t, E.Of[error](0), result)
|
||||
}
|
||||
|
||||
func TestTailRec_StackSafetyWithCancellation(t *testing.T) {
|
||||
// Test stack safety with cancellation after many iterations
|
||||
const largeN = 100000
|
||||
var iterationCount int32
|
||||
|
||||
countdownStep := func(n int) ReaderIOResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) IOEither[E.Either[int, int]] {
|
||||
return func() Either[E.Either[int, int]] {
|
||||
atomic.AddInt32(&iterationCount, 1)
|
||||
|
||||
// Add a small delay every 1000 iterations to make cancellation more likely
|
||||
if n%1000 == 0 {
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
}
|
||||
|
||||
if n <= 0 {
|
||||
return E.Right[error](E.Right[int](0))
|
||||
}
|
||||
return E.Right[error](E.Left[int](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
|
||||
// Cancel after 50ms to allow some iterations but not all
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
result := countdown(largeN)(ctx)()
|
||||
|
||||
// Should be cancelled (or completed if very fast)
|
||||
// The key is that it doesn't cause a stack overflow
|
||||
iterations := atomic.LoadInt32(&iterationCount)
|
||||
assert.Greater(t, iterations, int32(0))
|
||||
|
||||
// If it was cancelled, verify it didn't complete all iterations
|
||||
if E.IsLeft(result) {
|
||||
assert.Less(t, iterations, int32(largeN))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTailRec_ComplexState(t *testing.T) {
|
||||
// Test with more complex state management
|
||||
type ProcessState struct {
|
||||
items []string
|
||||
processed []string
|
||||
errors []error
|
||||
}
|
||||
|
||||
processStep := func(state ProcessState) ReaderIOResult[E.Either[ProcessState, []string]] {
|
||||
return func(ctx context.Context) IOEither[E.Either[ProcessState, []string]] {
|
||||
return func() Either[E.Either[ProcessState, []string]] {
|
||||
if len(state.items) == 0 {
|
||||
return E.Right[error](E.Right[ProcessState](state.processed))
|
||||
}
|
||||
|
||||
item := state.items[0]
|
||||
|
||||
// Simulate processing that might fail for certain items
|
||||
if item == "error-item" {
|
||||
return E.Left[E.Either[ProcessState, []string]](
|
||||
fmt.Errorf("failed to process item: %s", item))
|
||||
}
|
||||
|
||||
return E.Right[error](E.Left[[]string](ProcessState{
|
||||
items: state.items[1:],
|
||||
processed: append(state.processed, item),
|
||||
errors: state.errors,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processItems := TailRec(processStep)
|
||||
|
||||
t.Run("successful processing", func(t *testing.T) {
|
||||
initialState := ProcessState{
|
||||
items: []string{"item1", "item2", "item3"},
|
||||
processed: []string{},
|
||||
errors: []error{},
|
||||
}
|
||||
|
||||
result := processItems(initialState)(context.Background())()
|
||||
|
||||
assert.Equal(t, E.Of[error]([]string{"item1", "item2", "item3"}), result)
|
||||
})
|
||||
|
||||
t.Run("processing with error", func(t *testing.T) {
|
||||
initialState := ProcessState{
|
||||
items: []string{"item1", "error-item", "item3"},
|
||||
processed: []string{},
|
||||
errors: []error{},
|
||||
}
|
||||
|
||||
result := processItems(initialState)(context.Background())()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
err := E.ToError(result)
|
||||
assert.Contains(t, err.Error(), "failed to process item: error-item")
|
||||
})
|
||||
}
|
||||
|
||||
func TestTailRec_CancellationDuringProcessing(t *testing.T) {
|
||||
// Test cancellation during a realistic processing scenario
|
||||
type FileProcessState struct {
|
||||
files []string
|
||||
processed int
|
||||
}
|
||||
|
||||
var processedCount int32
|
||||
|
||||
processFileStep := func(state FileProcessState) ReaderIOResult[E.Either[FileProcessState, int]] {
|
||||
return func(ctx context.Context) IOEither[E.Either[FileProcessState, int]] {
|
||||
return func() Either[E.Either[FileProcessState, int]] {
|
||||
if len(state.files) == 0 {
|
||||
return E.Right[error](E.Right[FileProcessState](state.processed))
|
||||
}
|
||||
|
||||
// Simulate file processing time
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
atomic.AddInt32(&processedCount, 1)
|
||||
|
||||
return E.Right[error](E.Left[int](FileProcessState{
|
||||
files: state.files[1:],
|
||||
processed: state.processed + 1,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processFiles := TailRec(processFileStep)
|
||||
|
||||
// Create many files to process
|
||||
files := make([]string, 20)
|
||||
for i := range files {
|
||||
files[i] = fmt.Sprintf("file%d.txt", i)
|
||||
}
|
||||
|
||||
initialState := FileProcessState{
|
||||
files: files,
|
||||
processed: 0,
|
||||
}
|
||||
|
||||
// Cancel after 100ms (should allow ~5 files to be processed)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
start := time.Now()
|
||||
result := processFiles(initialState)(ctx)()
|
||||
elapsed := time.Since(start)
|
||||
|
||||
// Should be cancelled
|
||||
assert.True(t, E.IsLeft(result))
|
||||
|
||||
// Should complete quickly due to cancellation
|
||||
assert.Less(t, elapsed, 150*time.Millisecond)
|
||||
|
||||
// Should have processed some but not all files
|
||||
processed := atomic.LoadInt32(&processedCount)
|
||||
assert.Greater(t, processed, int32(0))
|
||||
assert.Less(t, processed, int32(20))
|
||||
}
|
||||
|
||||
func TestTailRec_ZeroIterations(t *testing.T) {
|
||||
// Test case where recursion terminates immediately
|
||||
immediateStep := func(n int) ReaderIOResult[E.Either[int, string]] {
|
||||
return func(ctx context.Context) IOEither[E.Either[int, string]] {
|
||||
return func() Either[E.Either[int, string]] {
|
||||
return E.Right[error](E.Right[int]("immediate"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
immediate := TailRec(immediateStep)
|
||||
result := immediate(100)(context.Background())()
|
||||
|
||||
assert.Equal(t, E.Of[error]("immediate"), result)
|
||||
}
|
||||
|
||||
func TestTailRec_ContextWithDeadline(t *testing.T) {
|
||||
// Test with context deadline
|
||||
var iterationCount int32
|
||||
|
||||
slowStep := func(n int) ReaderIOResult[E.Either[int, string]] {
|
||||
return func(ctx context.Context) IOEither[E.Either[int, string]] {
|
||||
return func() Either[E.Either[int, string]] {
|
||||
atomic.AddInt32(&iterationCount, 1)
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
|
||||
if n <= 0 {
|
||||
return E.Right[error](E.Right[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](E.Left[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slowRecursion := TailRec(slowStep)
|
||||
|
||||
// Set deadline 80ms from now
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(80*time.Millisecond))
|
||||
defer cancel()
|
||||
|
||||
result := slowRecursion(10)(ctx)()
|
||||
|
||||
// Should be cancelled due to deadline
|
||||
assert.True(t, E.IsLeft(result))
|
||||
|
||||
// Should have executed only a few iterations
|
||||
iterations := atomic.LoadInt32(&iterationCount)
|
||||
assert.Greater(t, iterations, int32(0))
|
||||
assert.Less(t, iterations, int32(5))
|
||||
}
|
||||
|
||||
func TestTailRec_ContextWithValue(t *testing.T) {
|
||||
// Test that context values are preserved through recursion
|
||||
type contextKey string
|
||||
const testKey contextKey = "test"
|
||||
|
||||
valueStep := func(n int) ReaderIOResult[E.Either[int, string]] {
|
||||
return func(ctx context.Context) IOEither[E.Either[int, string]] {
|
||||
return func() Either[E.Either[int, string]] {
|
||||
value := ctx.Value(testKey)
|
||||
require.NotNil(t, value)
|
||||
assert.Equal(t, "test-value", value.(string))
|
||||
|
||||
if n <= 0 {
|
||||
return E.Right[error](E.Right[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](E.Left[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
valueRecursion := TailRec(valueStep)
|
||||
ctx := context.WithValue(context.Background(), testKey, "test-value")
|
||||
result := valueRecursion(3)(ctx)()
|
||||
|
||||
assert.Equal(t, E.Of[error]("Done!"), result)
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package readerioresult
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/record"
|
||||
)
|
||||
|
||||
@@ -34,7 +35,7 @@ func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
Map[[]B, func(B) []B],
|
||||
Ap[[]B, B],
|
||||
|
||||
f,
|
||||
F.Flow2(f, WithContext),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -78,7 +79,7 @@ func TraverseRecord[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, ma
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
Ap[map[K]B, B],
|
||||
|
||||
f,
|
||||
F.Flow2(f, WithContext),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -123,7 +124,7 @@ func MonadTraverseArraySeq[A, B any](as []A, f Kleisli[A, B]) ReaderIOResult[[]B
|
||||
Map[[]B, func(B) []B],
|
||||
ApSeq[[]B, B],
|
||||
as,
|
||||
f,
|
||||
F.Flow2(f, WithContext),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -139,7 +140,7 @@ func TraverseArraySeq[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
ApSeq[[]B, B],
|
||||
f,
|
||||
F.Flow2(f, WithContext),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -171,7 +172,7 @@ func MonadTraverseRecordSeq[K comparable, A, B any](as map[K]A, f Kleisli[A, B])
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
ApSeq[map[K]B, B],
|
||||
as,
|
||||
f,
|
||||
F.Flow2(f, WithContext),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -182,7 +183,7 @@ func TraverseRecordSeq[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A,
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
ApSeq[map[K]B, B],
|
||||
|
||||
f,
|
||||
F.Flow2(f, WithContext),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -216,7 +217,7 @@ func MonadTraverseArrayPar[A, B any](as []A, f Kleisli[A, B]) ReaderIOResult[[]B
|
||||
Map[[]B, func(B) []B],
|
||||
ApPar[[]B, B],
|
||||
as,
|
||||
f,
|
||||
F.Flow2(f, WithContext),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -232,7 +233,7 @@ func TraverseArrayPar[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
ApPar[[]B, B],
|
||||
f,
|
||||
F.Flow2(f, WithContext),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -264,7 +265,7 @@ func TraverseRecordPar[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A,
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
ApPar[map[K]B, B],
|
||||
|
||||
f,
|
||||
F.Flow2(f, WithContext),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -286,7 +287,7 @@ func MonadTraverseRecordPar[K comparable, A, B any](as map[K]A, f Kleisli[A, B])
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
ApPar[map[K]B, B],
|
||||
as,
|
||||
f,
|
||||
F.Flow2(f, WithContext),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,11 @@ package readerioresult
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/consumer"
|
||||
"github.com/IBM/fp-go/v2/context/ioresult"
|
||||
"github.com/IBM/fp-go/v2/context/readerresult"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
@@ -126,4 +128,8 @@ type (
|
||||
ReaderResult[A any] = readerresult.ReaderResult[A]
|
||||
ReaderEither[R, E, A any] = readereither.ReaderEither[R, E, A]
|
||||
ReaderOption[R, A any] = readeroption.ReaderOption[R, A]
|
||||
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
Consumer[A any] = consumer.Consumer[A]
|
||||
)
|
||||
|
||||
@@ -15,11 +15,14 @@
|
||||
|
||||
package readerresult
|
||||
|
||||
import "github.com/IBM/fp-go/v2/readereither"
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/readereither"
|
||||
)
|
||||
|
||||
// TraverseArray transforms an array
|
||||
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
return readereither.TraverseArray(f)
|
||||
return readereither.TraverseArray(F.Flow2(f, WithContext))
|
||||
}
|
||||
|
||||
// TraverseArrayWithIndex transforms an array
|
||||
|
||||
@@ -31,6 +31,8 @@ import (
|
||||
// TenantID string
|
||||
// }
|
||||
// result := readereither.Do(State{})
|
||||
//
|
||||
//go:inline
|
||||
func Do[S any](
|
||||
empty S,
|
||||
) ReaderResult[S] {
|
||||
@@ -78,14 +80,18 @@ func Do[S any](
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f Kleisli[S1, T],
|
||||
) Kleisli[ReaderResult[S1], S2] {
|
||||
return G.Bind[ReaderResult[S1], ReaderResult[S2]](setter, f)
|
||||
return G.Bind[ReaderResult[S1], ReaderResult[S2]](setter, F.Flow2(f, WithContext))
|
||||
}
|
||||
|
||||
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
//
|
||||
//go:inline
|
||||
func Let[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
@@ -94,6 +100,8 @@ func Let[S1, S2, T any](
|
||||
}
|
||||
|
||||
// LetTo attaches the a value to a context [S1] to produce a context [S2]
|
||||
//
|
||||
//go:inline
|
||||
func LetTo[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
@@ -102,6 +110,8 @@ func LetTo[S1, S2, T any](
|
||||
}
|
||||
|
||||
// BindTo initializes a new state [S1] from a value [T]
|
||||
//
|
||||
//go:inline
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
) Kleisli[ReaderResult[T], S1] {
|
||||
@@ -145,6 +155,8 @@ func BindTo[S1, T any](
|
||||
// getTenantID,
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa ReaderResult[T],
|
||||
@@ -183,6 +195,8 @@ func ApS[S1, S2, T any](
|
||||
// readereither.Do(Person{Name: "Alice", Age: 25}),
|
||||
// readereither.ApSL(ageLens, getAge),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa ReaderResult[T],
|
||||
@@ -227,11 +241,13 @@ func ApSL[S, T any](
|
||||
// readereither.Of[error](Counter{Value: 42}),
|
||||
// readereither.BindL(valueLens, increment),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func BindL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Kleisli[T, T],
|
||||
) Kleisli[ReaderResult[S], S] {
|
||||
return Bind(lens.Set, F.Flow2(lens.Get, f))
|
||||
return Bind(lens.Set, F.Flow2(lens.Get, F.Flow2(f, WithContext)))
|
||||
}
|
||||
|
||||
// LetL is a variant of Let that uses a lens to focus on a specific field in the state.
|
||||
@@ -262,9 +278,11 @@ func BindL[S, T any](
|
||||
// readereither.LetL(valueLens, double),
|
||||
// )
|
||||
// // result when executed will be Right(Counter{Value: 42})
|
||||
//
|
||||
//go:inline
|
||||
func LetL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f func(T) T,
|
||||
f Endomorphism[T],
|
||||
) Kleisli[ReaderResult[S], S] {
|
||||
return Let(lens.Set, F.Flow2(lens.Get, f))
|
||||
}
|
||||
@@ -296,6 +314,8 @@ func LetL[S, T any](
|
||||
// readereither.LetToL(debugLens, false),
|
||||
// )
|
||||
// // result when executed will be Right(Config{Debug: false, Timeout: 30})
|
||||
//
|
||||
//go:inline
|
||||
func LetToL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
b T,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
154
v2/context/readerresult/flip.go
Normal file
154
v2/context/readerresult/flip.go
Normal file
@@ -0,0 +1,154 @@
|
||||
// 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/reader"
|
||||
RR "github.com/IBM/fp-go/v2/readerresult"
|
||||
)
|
||||
|
||||
// SequenceReader swaps the order of environment parameters when the inner computation is a Reader.
|
||||
//
|
||||
// This function is specialized for the context.Context-based ReaderResult monad. It takes a
|
||||
// ReaderResult that produces a Reader and returns a reader.Kleisli that produces Results.
|
||||
// The context.Context is implicitly used as the outer environment type.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The inner environment type (becomes outer after flip)
|
||||
// - A: The success value type
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: A ReaderResult that takes context.Context and may produce a Reader[R, A]
|
||||
//
|
||||
// Returns:
|
||||
// - A reader.Kleisli[context.Context, R, Result[A]], which is func(context.Context) func(R) Result[A]
|
||||
//
|
||||
// The function preserves error handling from the outer ReaderResult layer. If the outer
|
||||
// computation fails, the error is propagated to the inner Result.
|
||||
//
|
||||
// Note: This is an inline wrapper around readerresult.SequenceReader, specialized for
|
||||
// context.Context as the outer environment type.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Database struct {
|
||||
// ConnectionString string
|
||||
// }
|
||||
//
|
||||
// // Original: takes context, may fail, produces Reader[Database, string]
|
||||
// original := func(ctx context.Context) result.Result[reader.Reader[Database, string]] {
|
||||
// if ctx.Err() != nil {
|
||||
// return result.Error[reader.Reader[Database, string]](ctx.Err())
|
||||
// }
|
||||
// return result.Ok[error](func(db Database) string {
|
||||
// return fmt.Sprintf("Query on %s", db.ConnectionString)
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// // Sequenced: takes context first, then Database
|
||||
// sequenced := SequenceReader(original)
|
||||
//
|
||||
// ctx := context.Background()
|
||||
// db := Database{ConnectionString: "localhost:5432"}
|
||||
//
|
||||
// // Apply context first to get a function that takes database
|
||||
// dbReader := sequenced(ctx)
|
||||
// // Then apply database to get the final result
|
||||
// result := dbReader(db)
|
||||
// // result is Result[string]
|
||||
//
|
||||
// Use Cases:
|
||||
// - Dependency injection: Flip parameter order to inject context first, then dependencies
|
||||
// - Testing: Separate context handling from business logic for easier testing
|
||||
// - Composition: Enable point-free style by fixing the context parameter first
|
||||
//
|
||||
//go:inline
|
||||
func SequenceReader[R, A any](ma ReaderResult[Reader[R, A]]) reader.Kleisli[context.Context, R, Result[A]] {
|
||||
return RR.SequenceReader(ma)
|
||||
}
|
||||
|
||||
// TraverseReader transforms a value using a Reader function and swaps environment parameter order.
|
||||
//
|
||||
// This function combines mapping and parameter flipping in a single operation. It takes a
|
||||
// Reader function (pure computation without error handling) and returns a function that:
|
||||
// 1. Maps a ReaderResult[A] to ReaderResult[B] using the provided Reader function
|
||||
// 2. Flips the parameter order so R comes before context.Context
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The inner environment type (becomes outer after flip)
|
||||
// - A: The input value type
|
||||
// - B: The output value type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A reader.Kleisli[R, A, B], which is func(R) func(A) B - a pure Reader function
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes ReaderResult[A] and returns Kleisli[R, B]
|
||||
// - Kleisli[R, B] is func(R) ReaderResult[B], which is func(R) func(context.Context) Result[B]
|
||||
//
|
||||
// The function preserves error handling from the input ReaderResult. If the input computation
|
||||
// fails, the error is propagated without applying the transformation function.
|
||||
//
|
||||
// Note: This is a wrapper around readerresult.TraverseReader, specialized for context.Context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// MaxRetries int
|
||||
// }
|
||||
//
|
||||
// // A pure Reader function that depends on Config
|
||||
// formatMessage := func(cfg Config) func(int) string {
|
||||
// return func(value int) string {
|
||||
// return fmt.Sprintf("Value: %d, MaxRetries: %d", value, cfg.MaxRetries)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Original computation that may fail
|
||||
// computation := func(ctx context.Context) result.Result[int] {
|
||||
// if ctx.Err() != nil {
|
||||
// return result.Error[int](ctx.Err())
|
||||
// }
|
||||
// return result.Ok[error](42)
|
||||
// }
|
||||
//
|
||||
// // Create a traversal that applies formatMessage and flips parameters
|
||||
// traverse := TraverseReader[Config, int, string](formatMessage)
|
||||
//
|
||||
// // Apply to the computation
|
||||
// flipped := traverse(computation)
|
||||
//
|
||||
// // Now we can provide Config first, then context
|
||||
// cfg := Config{MaxRetries: 3}
|
||||
// ctx := context.Background()
|
||||
//
|
||||
// result := flipped(cfg)(ctx)
|
||||
// // result is Result[string] containing "Value: 42, MaxRetries: 3"
|
||||
//
|
||||
// Use Cases:
|
||||
// - Dependency injection: Inject configuration/dependencies before context
|
||||
// - Testing: Separate pure business logic from context handling
|
||||
// - Composition: Build pipelines where dependencies are fixed before execution
|
||||
// - Point-free style: Enable partial application by fixing dependencies first
|
||||
//
|
||||
//go:inline
|
||||
func TraverseReader[R, A, B any](
|
||||
f reader.Kleisli[R, A, B],
|
||||
) func(ReaderResult[A]) Kleisli[R, B] {
|
||||
return RR.TraverseReader[context.Context](f)
|
||||
}
|
||||
215
v2/context/readerresult/logging.go
Normal file
215
v2/context/readerresult/logging.go
Normal file
@@ -0,0 +1,215 @@
|
||||
// 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 logging utilities for the ReaderResult monad,
|
||||
// which combines the Reader monad (for dependency injection via context.Context)
|
||||
// with the Result monad (for error handling).
|
||||
//
|
||||
// The logging functions in this package allow you to log Result values (both
|
||||
// successes and errors) while preserving the functional composition style.
|
||||
package readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/logging"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// curriedLog creates a curried logging function that takes an slog.Attr and a context,
|
||||
// then logs the attribute with the specified log level and message.
|
||||
//
|
||||
// This is an internal helper function used to create the logging pipeline in a
|
||||
// point-free style. The currying allows for partial application in functional
|
||||
// composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - logLevel: The slog.Level at which to log (e.g., LevelInfo, LevelError)
|
||||
// - cb: A callback function that retrieves a logger from the context
|
||||
// - message: The log message to display
|
||||
//
|
||||
// Returns:
|
||||
// - A curried function that takes an slog.Attr, then a context, and performs logging
|
||||
func curriedLog(
|
||||
logLevel slog.Level,
|
||||
cb func(context.Context) *slog.Logger,
|
||||
message string) func(slog.Attr) Reader[context.Context, struct{}] {
|
||||
return F.Curry2(func(a slog.Attr, ctx context.Context) struct{} {
|
||||
cb(ctx).LogAttrs(ctx, logLevel, message, a)
|
||||
return struct{}{}
|
||||
})
|
||||
}
|
||||
|
||||
// SLogWithCallback creates a Kleisli arrow that logs a Result value using a custom
|
||||
// logger callback and log level. The Result value is logged and then returned unchanged,
|
||||
// making this function suitable for use in functional pipelines.
|
||||
//
|
||||
// This function logs both successful values and errors:
|
||||
// - Success values are logged with the key "value"
|
||||
// - Error values are logged with the key "error"
|
||||
//
|
||||
// The logging is performed as a side effect while preserving the Result value,
|
||||
// allowing it to be used in the middle of a computation pipeline without
|
||||
// interrupting the flow.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the success value in the Result
|
||||
//
|
||||
// Parameters:
|
||||
// - logLevel: The slog.Level at which to log (e.g., LevelInfo, LevelDebug, LevelError)
|
||||
// - cb: A callback function that retrieves a *slog.Logger from the context
|
||||
// - message: The log message to display
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a Result[A] and returns a ReaderResult[A]
|
||||
// The returned ReaderResult, when executed with a context, logs the Result
|
||||
// and returns it unchanged
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type User struct {
|
||||
// ID int
|
||||
// Name string
|
||||
// }
|
||||
//
|
||||
// // Custom logger callback
|
||||
// getLogger := func(ctx context.Context) *slog.Logger {
|
||||
// return slog.Default()
|
||||
// }
|
||||
//
|
||||
// // Create a logging function for debug level
|
||||
// logDebug := SLogWithCallback[User](slog.LevelDebug, getLogger, "User data")
|
||||
//
|
||||
// // Use in a pipeline
|
||||
// ctx := context.Background()
|
||||
// user := result.Of(User{ID: 123, Name: "Alice"})
|
||||
// logged := logDebug(user)(ctx) // Logs: level=DEBUG msg="User data" value={ID:123 Name:Alice}
|
||||
// // logged still contains the User value
|
||||
//
|
||||
// Example with error:
|
||||
//
|
||||
// err := errors.New("user not found")
|
||||
// userResult := result.Left[User](err)
|
||||
// logged := logDebug(userResult)(ctx) // Logs: level=DEBUG msg="User data" error="user not found"
|
||||
// // logged still contains the error
|
||||
func SLogWithCallback[A any](
|
||||
logLevel slog.Level,
|
||||
cb func(context.Context) *slog.Logger,
|
||||
message string) Kleisli[Result[A], A] {
|
||||
|
||||
return F.Pipe1(
|
||||
F.Flow2(
|
||||
result.ToSLogAttr[A](),
|
||||
curriedLog(logLevel, cb, message),
|
||||
),
|
||||
reader.Chain(reader.Sequence(F.Flow2( // this flow is basically the `MapTo` function with side effects
|
||||
reader.Of[struct{}, Result[A]],
|
||||
reader.Map[context.Context, struct{}, Result[A]],
|
||||
))),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
// SLog creates a Kleisli arrow that logs a Result value at INFO level using the
|
||||
// logger from the context. This is a convenience function that uses SLogWithCallback
|
||||
// with default settings.
|
||||
//
|
||||
// The Result value is logged and then returned unchanged, making this function
|
||||
// suitable for use in functional pipelines for debugging or monitoring purposes.
|
||||
//
|
||||
// This function logs both successful values and errors:
|
||||
// - Success values are logged with the key "value"
|
||||
// - Error values are logged with the key "error"
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the success value in the Result
|
||||
//
|
||||
// Parameters:
|
||||
// - message: The log message to display
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a Result[A] and returns a ReaderResult[A]
|
||||
// The returned ReaderResult, when executed with a context, logs the Result
|
||||
// at INFO level and returns it unchanged
|
||||
//
|
||||
// Example - Logging a successful computation:
|
||||
//
|
||||
// ctx := context.Background()
|
||||
//
|
||||
// // Simple value logging
|
||||
// res := result.Of(42)
|
||||
// logged := SLog[int]("Processing number")(res)(ctx)
|
||||
// // Logs: level=INFO msg="Processing number" value=42
|
||||
// // logged == result.Of(42)
|
||||
//
|
||||
// Example - Logging in a pipeline:
|
||||
//
|
||||
// type User struct {
|
||||
// ID int
|
||||
// Name string
|
||||
// }
|
||||
//
|
||||
// fetchUser := func(id int) result.Result[User] {
|
||||
// return result.Of(User{ID: id, Name: "Alice"})
|
||||
// }
|
||||
//
|
||||
// processUser := func(user User) result.Result[string] {
|
||||
// return result.Of(fmt.Sprintf("Processed: %s", user.Name))
|
||||
// }
|
||||
//
|
||||
// ctx := context.Background()
|
||||
//
|
||||
// // Log at each step
|
||||
// userResult := fetchUser(123)
|
||||
// logged1 := SLog[User]("Fetched user")(userResult)(ctx)
|
||||
// // Logs: level=INFO msg="Fetched user" value={ID:123 Name:Alice}
|
||||
//
|
||||
// processed := result.Chain(processUser)(logged1)
|
||||
// logged2 := SLog[string]("Processed user")(processed)(ctx)
|
||||
// // Logs: level=INFO msg="Processed user" value="Processed: Alice"
|
||||
//
|
||||
// Example - Logging errors:
|
||||
//
|
||||
// err := errors.New("database connection failed")
|
||||
// errResult := result.Left[User](err)
|
||||
// logged := SLog[User]("Database operation")(errResult)(ctx)
|
||||
// // Logs: level=INFO msg="Database operation" error="database connection failed"
|
||||
// // logged still contains the error
|
||||
//
|
||||
// Example - Using with context logger:
|
||||
//
|
||||
// // Set up a custom logger in the context
|
||||
// logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||
// ctx := logging.WithLogger(logger)(context.Background())
|
||||
//
|
||||
// res := result.Of("important data")
|
||||
// logged := SLog[string]("Critical operation")(res)(ctx)
|
||||
// // Uses the logger from context to log the message
|
||||
//
|
||||
// Note: The function uses logging.GetLoggerFromContext to retrieve the logger,
|
||||
// which falls back to the global logger if no logger is found in the context.
|
||||
//
|
||||
//go:inline
|
||||
func SLog[A any](message string) Kleisli[Result[A], A] {
|
||||
return SLogWithCallback[A](slog.LevelInfo, logging.GetLoggerFromContext, message)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TapSLog[A any](message string) Operator[A, A] {
|
||||
return reader.Chain(SLog[A](message))
|
||||
}
|
||||
302
v2/context/readerresult/logging_test.go
Normal file
302
v2/context/readerresult/logging_test.go
Normal file
@@ -0,0 +1,302 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/logging"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestSLogLogsSuccessValue tests that SLog logs successful Result values
|
||||
func TestSLogLogsSuccessValue(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a Result and log it
|
||||
res1 := result.Of(42)
|
||||
logged := SLog[int]("Result value")(res1)(ctx)
|
||||
|
||||
assert.Equal(t, result.Of(42), logged)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Result value")
|
||||
assert.Contains(t, logOutput, "value=42")
|
||||
}
|
||||
|
||||
// TestSLogLogsErrorValue tests that SLog logs error Result values
|
||||
func TestSLogLogsErrorValue(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
testErr := errors.New("test error")
|
||||
|
||||
// Create an error Result and log it
|
||||
res1 := result.Left[int](testErr)
|
||||
logged := SLog[int]("Result value")(res1)(ctx)
|
||||
|
||||
assert.Equal(t, res1, logged)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Result value")
|
||||
assert.Contains(t, logOutput, "error")
|
||||
assert.Contains(t, logOutput, "test error")
|
||||
}
|
||||
|
||||
// TestSLogInPipeline tests SLog in a functional pipeline
|
||||
func TestSLogInPipeline(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// SLog takes a Result[A] and returns ReaderResult[A]
|
||||
// So we need to start with a Result, apply SLog, then execute with context
|
||||
res1 := result.Of(10)
|
||||
logged := SLog[int]("Initial value")(res1)(ctx)
|
||||
|
||||
assert.Equal(t, result.Of(10), logged)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Initial value")
|
||||
assert.Contains(t, logOutput, "value=10")
|
||||
}
|
||||
|
||||
// TestSLogWithContextLogger tests SLog using logger from context
|
||||
func TestSLogWithContextLogger(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
contextLogger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
|
||||
ctx := logging.WithLogger(contextLogger)(context.Background())
|
||||
|
||||
res1 := result.Of("test value")
|
||||
logged := SLog[string]("Context logger test")(res1)(ctx)
|
||||
|
||||
assert.Equal(t, result.Of("test value"), logged)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Context logger test")
|
||||
assert.Contains(t, logOutput, `value="test value"`)
|
||||
}
|
||||
|
||||
// TestSLogDisabled tests that SLog respects logger level
|
||||
func TestSLogDisabled(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
// Create logger with level that disables info logs
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelError, // Only log errors
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
res1 := result.Of(42)
|
||||
logged := SLog[int]("This should not be logged")(res1)(ctx)
|
||||
|
||||
assert.Equal(t, result.Of(42), logged)
|
||||
|
||||
// Should have no logs since level is ERROR
|
||||
logOutput := buf.String()
|
||||
assert.Empty(t, logOutput, "Should have no logs when logging is disabled")
|
||||
}
|
||||
|
||||
// TestSLogWithStruct tests SLog with structured data
|
||||
func TestSLogWithStruct(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
user := User{ID: 123, Name: "Alice"}
|
||||
|
||||
res1 := result.Of(user)
|
||||
logged := SLog[User]("User data")(res1)(ctx)
|
||||
|
||||
assert.Equal(t, result.Of(user), logged)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "User data")
|
||||
assert.Contains(t, logOutput, "ID:123")
|
||||
assert.Contains(t, logOutput, "Name:Alice")
|
||||
}
|
||||
|
||||
// TestSLogWithCallbackCustomLevel tests SLogWithCallback with custom log level
|
||||
func TestSLogWithCallbackCustomLevel(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelDebug,
|
||||
}))
|
||||
|
||||
customCallback := func(ctx context.Context) *slog.Logger {
|
||||
return logger
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a Result and log it with custom callback
|
||||
res1 := result.Of(42)
|
||||
logged := SLogWithCallback[int](slog.LevelDebug, customCallback, "Debug result")(res1)(ctx)
|
||||
|
||||
assert.Equal(t, result.Of(42), logged)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Debug result")
|
||||
assert.Contains(t, logOutput, "value=42")
|
||||
assert.Contains(t, logOutput, "level=DEBUG")
|
||||
}
|
||||
|
||||
// TestSLogWithCallbackLogsError tests SLogWithCallback logs errors
|
||||
func TestSLogWithCallbackLogsError(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelWarn,
|
||||
}))
|
||||
|
||||
customCallback := func(ctx context.Context) *slog.Logger {
|
||||
return logger
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
testErr := errors.New("warning error")
|
||||
|
||||
// Create an error Result and log it with custom callback
|
||||
res1 := result.Left[int](testErr)
|
||||
logged := SLogWithCallback[int](slog.LevelWarn, customCallback, "Warning result")(res1)(ctx)
|
||||
|
||||
assert.Equal(t, res1, logged)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Warning result")
|
||||
assert.Contains(t, logOutput, "error")
|
||||
assert.Contains(t, logOutput, "warning error")
|
||||
assert.Contains(t, logOutput, "level=WARN")
|
||||
}
|
||||
|
||||
// TestSLogChainedOperations tests SLog in chained operations
|
||||
func TestSLogChainedOperations(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// First log step 1
|
||||
res1 := result.Of(5)
|
||||
logged1 := SLog[int]("Step 1")(res1)(ctx)
|
||||
|
||||
// Then log step 2 with doubled value
|
||||
res2 := result.Map(N.Mul(2))(logged1)
|
||||
logged2 := SLog[int]("Step 2")(res2)(ctx)
|
||||
|
||||
assert.Equal(t, result.Of(10), logged2)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Step 1")
|
||||
assert.Contains(t, logOutput, "value=5")
|
||||
assert.Contains(t, logOutput, "Step 2")
|
||||
assert.Contains(t, logOutput, "value=10")
|
||||
}
|
||||
|
||||
// TestSLogPreservesError tests that SLog preserves error through the pipeline
|
||||
func TestSLogPreservesError(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
testErr := errors.New("original error")
|
||||
|
||||
res1 := result.Left[int](testErr)
|
||||
logged := SLog[int]("Logging error")(res1)(ctx)
|
||||
|
||||
// Apply map to verify error is preserved
|
||||
res2 := result.Map(N.Mul(2))(logged)
|
||||
|
||||
assert.Equal(t, res1, res2)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Logging error")
|
||||
assert.Contains(t, logOutput, "original error")
|
||||
}
|
||||
|
||||
// TestSLogMultipleValues tests logging multiple different values
|
||||
func TestSLogMultipleValues(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with different types
|
||||
intRes := SLog[int]("Integer")(result.Of(42))(ctx)
|
||||
assert.Equal(t, result.Of(42), intRes)
|
||||
|
||||
strRes := SLog[string]("String")(result.Of("hello"))(ctx)
|
||||
assert.Equal(t, result.Of("hello"), strRes)
|
||||
|
||||
boolRes := SLog[bool]("Boolean")(result.Of(true))(ctx)
|
||||
assert.Equal(t, result.Of(true), boolRes)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Integer")
|
||||
assert.Contains(t, logOutput, "value=42")
|
||||
assert.Contains(t, logOutput, "String")
|
||||
assert.Contains(t, logOutput, "value=hello")
|
||||
assert.Contains(t, logOutput, "Boolean")
|
||||
assert.Contains(t, logOutput, "value=true")
|
||||
}
|
||||
@@ -18,9 +18,17 @@ package readerresult
|
||||
import (
|
||||
"context"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/chain"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readereither"
|
||||
)
|
||||
|
||||
func FromReader[A any](r Reader[context.Context, A]) ReaderResult[A] {
|
||||
return readereither.FromReader[error](r)
|
||||
}
|
||||
|
||||
func FromEither[A any](e Either[A]) ReaderResult[A] {
|
||||
return readereither.FromEither[context.Context](e)
|
||||
}
|
||||
@@ -42,11 +50,11 @@ func Map[A, B any](f func(A) B) Operator[A, B] {
|
||||
}
|
||||
|
||||
func MonadChain[A, B any](ma ReaderResult[A], f Kleisli[A, B]) ReaderResult[B] {
|
||||
return readereither.MonadChain(ma, f)
|
||||
return readereither.MonadChain(ma, F.Flow2(f, WithContext))
|
||||
}
|
||||
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||
return readereither.Chain(f)
|
||||
return readereither.Chain(F.Flow2(f, WithContext))
|
||||
}
|
||||
|
||||
func Of[A any](a A) ReaderResult[A] {
|
||||
@@ -66,7 +74,7 @@ func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) Kleisli[A, A
|
||||
}
|
||||
|
||||
func OrElse[A any](onLeft Kleisli[error, A]) Kleisli[ReaderResult[A], A] {
|
||||
return readereither.OrElse(onLeft)
|
||||
return readereither.OrElse(F.Flow2(onLeft, WithContext))
|
||||
}
|
||||
|
||||
func Ask() ReaderResult[context.Context] {
|
||||
@@ -81,7 +89,7 @@ func ChainEitherK[A, B any](f func(A) Either[B]) func(ma ReaderResult[A]) Reader
|
||||
return readereither.ChainEitherK[context.Context](f)
|
||||
}
|
||||
|
||||
func ChainOptionK[A, B any](onNone func() error) func(func(A) Option[B]) Operator[A, B] {
|
||||
func ChainOptionK[A, B any](onNone func() error) func(option.Kleisli[A, B]) Operator[A, B] {
|
||||
return readereither.ChainOptionK[context.Context, A, B](onNone)
|
||||
}
|
||||
|
||||
@@ -97,3 +105,197 @@ func Flap[B, A any](a A) Operator[func(A) B, B] {
|
||||
func Read[A any](r context.Context) func(ReaderResult[A]) Result[A] {
|
||||
return readereither.Read[error, A](r)
|
||||
}
|
||||
|
||||
// MonadMapTo executes a ReaderResult computation, discards its success value, and returns a constant value.
|
||||
// This is the monadic version that takes both the ReaderResult and the constant value as parameters.
|
||||
//
|
||||
// IMPORTANT: ReaderResult represents a side-effectful computation because it depends on context.Context,
|
||||
// which is effectful (can be cancelled, has deadlines, carries values). For this reason, MonadMapTo WILL
|
||||
// execute the original ReaderResult to allow any side effects to occur, then discard the success result
|
||||
// and return the constant value. If the original computation fails, the error is preserved.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type of the first ReaderResult (will be discarded if successful)
|
||||
// - B: The type of the constant value to return on success
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The ReaderResult to execute (side effects will occur, success value discarded)
|
||||
// - b: The constant value to return if ma succeeds
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderResult that executes ma, preserves errors, but replaces success values with b
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Counter int }
|
||||
// increment := func(ctx context.Context) result.Result[int] {
|
||||
// // Side effect: log the operation
|
||||
// fmt.Println("incrementing")
|
||||
// return result.Of(5)
|
||||
// }
|
||||
// r := readerresult.MonadMapTo(increment, "done")
|
||||
// result := r(context.Background()) // Prints "incrementing", returns Right("done")
|
||||
//
|
||||
//go:inline
|
||||
func MonadMapTo[A, B any](ma ReaderResult[A], b B) ReaderResult[B] {
|
||||
return MonadMap(ma, reader.Of[A](b))
|
||||
}
|
||||
|
||||
// MapTo creates an operator that executes a ReaderResult computation, discards its success value,
|
||||
// and returns a constant value. This is the curried version where the constant value is provided first,
|
||||
// returning a function that can be applied to any ReaderResult.
|
||||
//
|
||||
// IMPORTANT: ReaderResult represents a side-effectful computation because it depends on context.Context,
|
||||
// which is effectful (can be cancelled, has deadlines, carries values). For this reason, MapTo WILL
|
||||
// execute the input ReaderResult to allow any side effects to occur, then discard the success result
|
||||
// and return the constant value. If the computation fails, the error is preserved.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type of the input ReaderResult (will be discarded if successful)
|
||||
// - B: The type of the constant value to return on success
|
||||
//
|
||||
// Parameters:
|
||||
// - b: The constant value to return on success
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that executes a ReaderResult[A], preserves errors, but replaces success with b
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// logStep := func(ctx context.Context) result.Result[int] {
|
||||
// fmt.Println("step executed")
|
||||
// return result.Of(42)
|
||||
// }
|
||||
// toDone := readerresult.MapTo[int, string]("done")
|
||||
// pipeline := toDone(logStep)
|
||||
// result := pipeline(context.Background()) // Prints "step executed", returns Right("done")
|
||||
//
|
||||
// Example - In a functional pipeline:
|
||||
//
|
||||
// step1 := func(ctx context.Context) result.Result[int] {
|
||||
// fmt.Println("processing")
|
||||
// return result.Of(1)
|
||||
// }
|
||||
// pipeline := F.Pipe1(
|
||||
// step1,
|
||||
// readerresult.MapTo[int, string]("complete"),
|
||||
// )
|
||||
// output := pipeline(context.Background()) // Prints "processing", returns Right("complete")
|
||||
//
|
||||
//go:inline
|
||||
func MapTo[A, B any](b B) Operator[A, B] {
|
||||
return Map(reader.Of[A](b))
|
||||
}
|
||||
|
||||
// MonadChainTo sequences two ReaderResult computations where the second ignores the first's success value.
|
||||
// This is the monadic version that takes both ReaderResults as parameters.
|
||||
//
|
||||
// IMPORTANT: ReaderResult represents a side-effectful computation because it depends on context.Context,
|
||||
// which is effectful (can be cancelled, has deadlines, carries values). For this reason, MonadChainTo WILL
|
||||
// execute the first ReaderResult to allow any side effects to occur, then discard the success result and
|
||||
// execute the second ReaderResult with the same context. If the first computation fails, the error is
|
||||
// returned immediately without executing the second computation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type of the first ReaderResult (will be discarded if successful)
|
||||
// - B: The success type of the second ReaderResult
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The first ReaderResult to execute (side effects will occur, success value discarded)
|
||||
// - b: The second ReaderResult to execute if ma succeeds
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderResult that executes ma, then b if ma succeeds, returning b's result
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// logStart := func(ctx context.Context) result.Result[int] {
|
||||
// fmt.Println("starting")
|
||||
// return result.Of(1)
|
||||
// }
|
||||
// logEnd := func(ctx context.Context) result.Result[string] {
|
||||
// fmt.Println("ending")
|
||||
// return result.Of("done")
|
||||
// }
|
||||
// r := readerresult.MonadChainTo(logStart, logEnd)
|
||||
// result := r(context.Background()) // Prints "starting" then "ending", returns Right("done")
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainTo[A, B any](ma ReaderResult[A], b ReaderResult[B]) ReaderResult[B] {
|
||||
return MonadChain(ma, reader.Of[A](b))
|
||||
}
|
||||
|
||||
// ChainTo creates an operator that sequences two ReaderResult computations where the second ignores
|
||||
// the first's success value. This is the curried version where the second ReaderResult is provided first,
|
||||
// returning a function that can be applied to any first ReaderResult.
|
||||
//
|
||||
// IMPORTANT: ReaderResult represents a side-effectful computation because it depends on context.Context,
|
||||
// which is effectful (can be cancelled, has deadlines, carries values). For this reason, ChainTo WILL
|
||||
// execute the first ReaderResult to allow any side effects to occur, then discard the success result and
|
||||
// execute the second ReaderResult with the same context. If the first computation fails, the error is
|
||||
// returned immediately without executing the second computation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type of the first ReaderResult (will be discarded if successful)
|
||||
// - B: The success type of the second ReaderResult
|
||||
//
|
||||
// Parameters:
|
||||
// - b: The second ReaderResult to execute after the first succeeds
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that executes the first ReaderResult, then b if successful
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// logEnd := func(ctx context.Context) result.Result[string] {
|
||||
// fmt.Println("ending")
|
||||
// return result.Of("done")
|
||||
// }
|
||||
// thenLogEnd := readerresult.ChainTo[int, string](logEnd)
|
||||
//
|
||||
// logStart := func(ctx context.Context) result.Result[int] {
|
||||
// fmt.Println("starting")
|
||||
// return result.Of(1)
|
||||
// }
|
||||
// pipeline := thenLogEnd(logStart)
|
||||
// result := pipeline(context.Background()) // Prints "starting" then "ending", returns Right("done")
|
||||
//
|
||||
// Example - In a functional pipeline:
|
||||
//
|
||||
// step1 := func(ctx context.Context) result.Result[int] {
|
||||
// fmt.Println("step 1")
|
||||
// return result.Of(1)
|
||||
// }
|
||||
// step2 := func(ctx context.Context) result.Result[string] {
|
||||
// fmt.Println("step 2")
|
||||
// return result.Of("complete")
|
||||
// }
|
||||
// pipeline := F.Pipe1(
|
||||
// step1,
|
||||
// readerresult.ChainTo[int, string](step2),
|
||||
// )
|
||||
// output := pipeline(context.Background()) // Prints "step 1" then "step 2", returns Right("complete")
|
||||
//
|
||||
//go:inline
|
||||
func ChainTo[A, B any](b ReaderResult[B]) Operator[A, B] {
|
||||
return Chain(reader.Of[A](b))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadChainFirst[A, B any](ma ReaderResult[A], f Kleisli[A, B]) ReaderResult[A] {
|
||||
return chain.MonadChainFirst(
|
||||
MonadChain,
|
||||
MonadMap,
|
||||
ma,
|
||||
F.Flow2(f, WithContext),
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
||||
return chain.ChainFirst(
|
||||
Chain,
|
||||
Map,
|
||||
F.Flow2(f, WithContext),
|
||||
)
|
||||
}
|
||||
|
||||
315
v2/context/readerresult/reader_test.go
Normal file
315
v2/context/readerresult/reader_test.go
Normal file
@@ -0,0 +1,315 @@
|
||||
// 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"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMapTo(t *testing.T) {
|
||||
t.Run("executes original reader and returns constant value on success", func(t *testing.T) {
|
||||
executed := false
|
||||
originalReader := func(ctx context.Context) E.Either[error, int] {
|
||||
executed = true
|
||||
return E.Of[error](42)
|
||||
}
|
||||
|
||||
// Apply MapTo operator
|
||||
toDone := MapTo[int]("done")
|
||||
resultReader := toDone(originalReader)
|
||||
|
||||
// Execute the resulting reader
|
||||
result := resultReader(context.Background())
|
||||
|
||||
// Verify the constant value is returned
|
||||
assert.Equal(t, E.Of[error]("done"), result)
|
||||
// Verify the original reader WAS executed (side effect occurred)
|
||||
assert.True(t, executed, "original reader should be executed to allow side effects")
|
||||
})
|
||||
|
||||
t.Run("executes reader in functional pipeline", func(t *testing.T) {
|
||||
executed := false
|
||||
step1 := func(ctx context.Context) E.Either[error, int] {
|
||||
executed = true
|
||||
return E.Of[error](100)
|
||||
}
|
||||
|
||||
pipeline := F.Pipe1(
|
||||
step1,
|
||||
MapTo[int]("complete"),
|
||||
)
|
||||
|
||||
result := pipeline(context.Background())
|
||||
|
||||
assert.Equal(t, E.Of[error]("complete"), result)
|
||||
assert.True(t, executed, "original reader should be executed in pipeline")
|
||||
})
|
||||
|
||||
t.Run("executes reader with side effects", func(t *testing.T) {
|
||||
sideEffectOccurred := false
|
||||
readerWithSideEffect := func(ctx context.Context) E.Either[error, int] {
|
||||
sideEffectOccurred = true
|
||||
return E.Of[error](42)
|
||||
}
|
||||
|
||||
resultReader := MapTo[int](true)(readerWithSideEffect)
|
||||
result := resultReader(context.Background())
|
||||
|
||||
assert.Equal(t, E.Of[error](true), result)
|
||||
assert.True(t, sideEffectOccurred, "side effect should occur")
|
||||
})
|
||||
|
||||
t.Run("preserves errors from original reader", func(t *testing.T) {
|
||||
executed := false
|
||||
testErr := assert.AnError
|
||||
failingReader := func(ctx context.Context) E.Either[error, int] {
|
||||
executed = true
|
||||
return E.Left[int](testErr)
|
||||
}
|
||||
|
||||
resultReader := MapTo[int]("done")(failingReader)
|
||||
result := resultReader(context.Background())
|
||||
|
||||
assert.Equal(t, E.Left[string](testErr), result)
|
||||
assert.True(t, executed, "failing reader should still be executed")
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadMapTo(t *testing.T) {
|
||||
t.Run("executes original reader and returns constant value on success", func(t *testing.T) {
|
||||
executed := false
|
||||
originalReader := func(ctx context.Context) E.Either[error, int] {
|
||||
executed = true
|
||||
return E.Of[error](42)
|
||||
}
|
||||
|
||||
// Apply MonadMapTo
|
||||
resultReader := MonadMapTo(originalReader, "done")
|
||||
|
||||
// Execute the resulting reader
|
||||
result := resultReader(context.Background())
|
||||
|
||||
// Verify the constant value is returned
|
||||
assert.Equal(t, E.Of[error]("done"), result)
|
||||
// Verify the original reader WAS executed (side effect occurred)
|
||||
assert.True(t, executed, "original reader should be executed to allow side effects")
|
||||
})
|
||||
|
||||
t.Run("executes complex computation with side effects", func(t *testing.T) {
|
||||
computationExecuted := false
|
||||
complexReader := func(ctx context.Context) E.Either[error, string] {
|
||||
computationExecuted = true
|
||||
return E.Of[error]("complex result")
|
||||
}
|
||||
|
||||
resultReader := MonadMapTo(complexReader, 42)
|
||||
result := resultReader(context.Background())
|
||||
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
assert.True(t, computationExecuted, "complex computation should be executed")
|
||||
})
|
||||
|
||||
t.Run("preserves errors from original reader", func(t *testing.T) {
|
||||
executed := false
|
||||
testErr := assert.AnError
|
||||
failingReader := func(ctx context.Context) E.Either[error, []string] {
|
||||
executed = true
|
||||
return E.Left[[]string](testErr)
|
||||
}
|
||||
|
||||
resultReader := MonadMapTo(failingReader, 99)
|
||||
result := resultReader(context.Background())
|
||||
|
||||
assert.Equal(t, E.Left[int](testErr), result)
|
||||
assert.True(t, executed, "failing reader should still be executed")
|
||||
})
|
||||
}
|
||||
|
||||
func TestChainTo(t *testing.T) {
|
||||
t.Run("executes first reader then second reader on success", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
secondExecuted := false
|
||||
|
||||
firstReader := func(ctx context.Context) E.Either[error, int] {
|
||||
firstExecuted = true
|
||||
return E.Of[error](42)
|
||||
}
|
||||
|
||||
secondReader := func(ctx context.Context) E.Either[error, string] {
|
||||
secondExecuted = true
|
||||
return E.Of[error]("result")
|
||||
}
|
||||
|
||||
// Apply ChainTo operator
|
||||
thenSecond := ChainTo[int](secondReader)
|
||||
resultReader := thenSecond(firstReader)
|
||||
|
||||
// Execute the resulting reader
|
||||
result := resultReader(context.Background())
|
||||
|
||||
// Verify the second reader's result is returned
|
||||
assert.Equal(t, E.Of[error]("result"), result)
|
||||
// Verify both readers were executed
|
||||
assert.True(t, firstExecuted, "first reader should be executed")
|
||||
assert.True(t, secondExecuted, "second reader should be executed")
|
||||
})
|
||||
|
||||
t.Run("executes both readers in functional pipeline", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
secondExecuted := false
|
||||
|
||||
step1 := func(ctx context.Context) E.Either[error, int] {
|
||||
firstExecuted = true
|
||||
return E.Of[error](100)
|
||||
}
|
||||
|
||||
step2 := func(ctx context.Context) E.Either[error, string] {
|
||||
secondExecuted = true
|
||||
return E.Of[error]("complete")
|
||||
}
|
||||
|
||||
pipeline := F.Pipe1(
|
||||
step1,
|
||||
ChainTo[int](step2),
|
||||
)
|
||||
|
||||
result := pipeline(context.Background())
|
||||
|
||||
assert.Equal(t, E.Of[error]("complete"), result)
|
||||
assert.True(t, firstExecuted, "first reader should be executed in pipeline")
|
||||
assert.True(t, secondExecuted, "second reader should be executed in pipeline")
|
||||
})
|
||||
|
||||
t.Run("executes first reader with side effects", func(t *testing.T) {
|
||||
sideEffectOccurred := false
|
||||
readerWithSideEffect := func(ctx context.Context) E.Either[error, int] {
|
||||
sideEffectOccurred = true
|
||||
return E.Of[error](42)
|
||||
}
|
||||
|
||||
secondReader := func(ctx context.Context) E.Either[error, bool] {
|
||||
return E.Of[error](true)
|
||||
}
|
||||
|
||||
resultReader := ChainTo[int](secondReader)(readerWithSideEffect)
|
||||
result := resultReader(context.Background())
|
||||
|
||||
assert.Equal(t, E.Of[error](true), result)
|
||||
assert.True(t, sideEffectOccurred, "side effect should occur in first reader")
|
||||
})
|
||||
|
||||
t.Run("preserves error from first reader without executing second", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
secondExecuted := false
|
||||
testErr := assert.AnError
|
||||
|
||||
failingReader := func(ctx context.Context) E.Either[error, int] {
|
||||
firstExecuted = true
|
||||
return E.Left[int](testErr)
|
||||
}
|
||||
|
||||
secondReader := func(ctx context.Context) E.Either[error, string] {
|
||||
secondExecuted = true
|
||||
return E.Of[error]("result")
|
||||
}
|
||||
|
||||
resultReader := ChainTo[int](secondReader)(failingReader)
|
||||
result := resultReader(context.Background())
|
||||
|
||||
assert.Equal(t, E.Left[string](testErr), result)
|
||||
assert.True(t, firstExecuted, "first reader should be executed")
|
||||
assert.False(t, secondExecuted, "second reader should not be executed on error")
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadChainTo(t *testing.T) {
|
||||
t.Run("executes first reader then second reader on success", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
secondExecuted := false
|
||||
|
||||
firstReader := func(ctx context.Context) E.Either[error, int] {
|
||||
firstExecuted = true
|
||||
return E.Of[error](42)
|
||||
}
|
||||
|
||||
secondReader := func(ctx context.Context) E.Either[error, string] {
|
||||
secondExecuted = true
|
||||
return E.Of[error]("result")
|
||||
}
|
||||
|
||||
// Apply MonadChainTo
|
||||
resultReader := MonadChainTo(firstReader, secondReader)
|
||||
|
||||
// Execute the resulting reader
|
||||
result := resultReader(context.Background())
|
||||
|
||||
// Verify the second reader's result is returned
|
||||
assert.Equal(t, E.Of[error]("result"), result)
|
||||
// Verify both readers were executed
|
||||
assert.True(t, firstExecuted, "first reader should be executed")
|
||||
assert.True(t, secondExecuted, "second reader should be executed")
|
||||
})
|
||||
|
||||
t.Run("executes complex first computation with side effects", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
secondExecuted := false
|
||||
|
||||
complexFirstReader := func(ctx context.Context) E.Either[error, []int] {
|
||||
firstExecuted = true
|
||||
return E.Of[error]([]int{1, 2, 3})
|
||||
}
|
||||
|
||||
secondReader := func(ctx context.Context) E.Either[error, string] {
|
||||
secondExecuted = true
|
||||
return E.Of[error]("done")
|
||||
}
|
||||
|
||||
resultReader := MonadChainTo(complexFirstReader, secondReader)
|
||||
result := resultReader(context.Background())
|
||||
|
||||
assert.Equal(t, E.Of[error]("done"), result)
|
||||
assert.True(t, firstExecuted, "complex first computation should be executed")
|
||||
assert.True(t, secondExecuted, "second reader should be executed")
|
||||
})
|
||||
|
||||
t.Run("preserves error from first reader without executing second", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
secondExecuted := false
|
||||
testErr := assert.AnError
|
||||
|
||||
failingReader := func(ctx context.Context) E.Either[error, map[string]int] {
|
||||
firstExecuted = true
|
||||
return E.Left[map[string]int](testErr)
|
||||
}
|
||||
|
||||
secondReader := func(ctx context.Context) E.Either[error, float64] {
|
||||
secondExecuted = true
|
||||
return E.Of[error](3.14)
|
||||
}
|
||||
|
||||
resultReader := MonadChainTo(failingReader, secondReader)
|
||||
result := resultReader(context.Background())
|
||||
|
||||
assert.Equal(t, E.Left[float64](testErr), result)
|
||||
assert.True(t, firstExecuted, "first reader should be executed")
|
||||
assert.False(t, secondExecuted, "second reader should not be executed on error")
|
||||
})
|
||||
}
|
||||
106
v2/context/readerresult/rec.go
Normal file
106
v2/context/readerresult/rec.go
Normal file
@@ -0,0 +1,106 @@
|
||||
// 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 implements a specialization of the Reader monad assuming a golang context as the context of the monad and a standard golang error
|
||||
package readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// TailRec implements tail-recursive computation for ReaderResult with context cancellation support.
|
||||
//
|
||||
// TailRec takes a Kleisli function that returns Either[A, B] and converts it into a stack-safe,
|
||||
// tail-recursive computation. The function repeatedly applies the Kleisli until it produces a Right value.
|
||||
//
|
||||
// The implementation includes a short-circuit mechanism that checks for context cancellation on each
|
||||
// iteration. If the context is canceled (ctx.Err() != nil), the computation immediately returns a
|
||||
// Left result containing the context's cause error, preventing unnecessary computation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The input type for the recursive step
|
||||
// - B: The final result type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Kleisli function that takes an A and returns a ReaderResult containing Either[A, B].
|
||||
// When the result is Left[B](a), recursion continues with the new value 'a'.
|
||||
// When the result is Right[A](b), recursion terminates with the final value 'b'.
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli function that performs the tail-recursive computation in a stack-safe manner.
|
||||
//
|
||||
// Behavior:
|
||||
// - On each iteration, checks if the context has been canceled (short circuit)
|
||||
// - If canceled, returns result.Left[B](context.Cause(ctx))
|
||||
// - If the step returns Left[B](error), propagates the error
|
||||
// - If the step returns Right[A](Left[B](a)), continues recursion with new value 'a'
|
||||
// - If the step returns Right[A](Right[A](b)), terminates with success value 'b'
|
||||
//
|
||||
// Example - Factorial computation with context:
|
||||
//
|
||||
// type State struct {
|
||||
// n int
|
||||
// acc int
|
||||
// }
|
||||
//
|
||||
// factorialStep := func(state State) ReaderResult[either.Either[State, int]] {
|
||||
// return func(ctx context.Context) result.Result[either.Either[State, int]] {
|
||||
// if state.n <= 0 {
|
||||
// return result.Of(either.Right[State](state.acc))
|
||||
// }
|
||||
// return result.Of(either.Left[int](State{state.n - 1, state.acc * state.n}))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// factorial := TailRec(factorialStep)
|
||||
// result := factorial(State{5, 1})(ctx) // Returns result.Of(120)
|
||||
//
|
||||
// Example - Context cancellation:
|
||||
//
|
||||
// ctx, cancel := context.WithCancel(context.Background())
|
||||
// cancel() // Cancel immediately
|
||||
//
|
||||
// computation := TailRec(someStep)
|
||||
// result := computation(initialValue)(ctx)
|
||||
// // Returns result.Left[B](context.Cause(ctx)) without executing any steps
|
||||
//
|
||||
//go:inline
|
||||
func TailRec[A, B any](f Kleisli[A, either.Either[A, B]]) Kleisli[A, B] {
|
||||
return func(a A) ReaderResult[B] {
|
||||
initialReader := f(a)
|
||||
return func(ctx context.Context) Result[B] {
|
||||
rdr := initialReader
|
||||
for {
|
||||
// short circuit
|
||||
if ctx.Err() != nil {
|
||||
return result.Left[B](context.Cause(ctx))
|
||||
}
|
||||
current := rdr(ctx)
|
||||
rec, e := either.Unwrap(current)
|
||||
if either.IsLeft(current) {
|
||||
return result.Left[B](e)
|
||||
}
|
||||
b, a := either.Unwrap(rec)
|
||||
if either.IsRight(rec) {
|
||||
return result.Of(b)
|
||||
}
|
||||
rdr = f(a)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
499
v2/context/readerresult/rec_test.go
Normal file
499
v2/context/readerresult/rec_test.go
Normal file
@@ -0,0 +1,499 @@
|
||||
// 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"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
R "github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestTailRecFactorial tests factorial computation with context
|
||||
func TestTailRecFactorial(t *testing.T) {
|
||||
type State struct {
|
||||
n int
|
||||
acc int
|
||||
}
|
||||
|
||||
factorialStep := func(state State) ReaderResult[E.Either[State, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[State, int]] {
|
||||
if state.n <= 0 {
|
||||
return R.Of(E.Right[State](state.acc))
|
||||
}
|
||||
return R.Of(E.Left[int](State{state.n - 1, state.acc * state.n}))
|
||||
}
|
||||
}
|
||||
|
||||
factorial := TailRec(factorialStep)
|
||||
result := factorial(State{5, 1})(context.Background())
|
||||
|
||||
assert.Equal(t, R.Of(120), result)
|
||||
}
|
||||
|
||||
// TestTailRecFibonacci tests Fibonacci computation
|
||||
func TestTailRecFibonacci(t *testing.T) {
|
||||
type State struct {
|
||||
n int
|
||||
prev int
|
||||
curr int
|
||||
}
|
||||
|
||||
fibStep := func(state State) ReaderResult[E.Either[State, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[State, int]] {
|
||||
if state.n <= 0 {
|
||||
return R.Of(E.Right[State](state.curr))
|
||||
}
|
||||
return R.Of(E.Left[int](State{state.n - 1, state.curr, state.prev + state.curr}))
|
||||
}
|
||||
}
|
||||
|
||||
fib := TailRec(fibStep)
|
||||
result := fib(State{10, 0, 1})(context.Background())
|
||||
|
||||
assert.Equal(t, R.Of(89), result) // 10th Fibonacci number
|
||||
}
|
||||
|
||||
// TestTailRecCountdown tests countdown computation
|
||||
func TestTailRecCountdown(t *testing.T) {
|
||||
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[int, int]] {
|
||||
if n <= 0 {
|
||||
return R.Of(E.Right[int](n))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(10)(context.Background())
|
||||
|
||||
assert.Equal(t, R.Of(0), result)
|
||||
}
|
||||
|
||||
// TestTailRecImmediateTermination tests immediate termination (Right on first call)
|
||||
func TestTailRecImmediateTermination(t *testing.T) {
|
||||
immediateStep := func(n int) ReaderResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[int, int]] {
|
||||
return R.Of(E.Right[int](n * 2))
|
||||
}
|
||||
}
|
||||
|
||||
immediate := TailRec(immediateStep)
|
||||
result := immediate(42)(context.Background())
|
||||
|
||||
assert.Equal(t, R.Of(84), result)
|
||||
}
|
||||
|
||||
// TestTailRecStackSafety tests that TailRec handles large iterations without stack overflow
|
||||
func TestTailRecStackSafety(t *testing.T) {
|
||||
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[int, int]] {
|
||||
if n <= 0 {
|
||||
return R.Of(E.Right[int](n))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(10000)(context.Background())
|
||||
|
||||
assert.Equal(t, R.Of(0), result)
|
||||
}
|
||||
|
||||
// TestTailRecSumList tests summing a list
|
||||
func TestTailRecSumList(t *testing.T) {
|
||||
type State struct {
|
||||
list []int
|
||||
sum int
|
||||
}
|
||||
|
||||
sumStep := func(state State) ReaderResult[E.Either[State, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[State, int]] {
|
||||
if len(state.list) == 0 {
|
||||
return R.Of(E.Right[State](state.sum))
|
||||
}
|
||||
return R.Of(E.Left[int](State{state.list[1:], state.sum + state.list[0]}))
|
||||
}
|
||||
}
|
||||
|
||||
sumList := TailRec(sumStep)
|
||||
result := sumList(State{[]int{1, 2, 3, 4, 5}, 0})(context.Background())
|
||||
|
||||
assert.Equal(t, R.Of(15), result)
|
||||
}
|
||||
|
||||
// TestTailRecCollatzConjecture tests the Collatz conjecture
|
||||
func TestTailRecCollatzConjecture(t *testing.T) {
|
||||
collatzStep := func(n int) ReaderResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[int, int]] {
|
||||
if n <= 1 {
|
||||
return R.Of(E.Right[int](n))
|
||||
}
|
||||
if n%2 == 0 {
|
||||
return R.Of(E.Left[int](n / 2))
|
||||
}
|
||||
return R.Of(E.Left[int](3*n + 1))
|
||||
}
|
||||
}
|
||||
|
||||
collatz := TailRec(collatzStep)
|
||||
result := collatz(10)(context.Background())
|
||||
|
||||
assert.Equal(t, R.Of(1), result)
|
||||
}
|
||||
|
||||
// TestTailRecGCD tests greatest common divisor
|
||||
func TestTailRecGCD(t *testing.T) {
|
||||
type State struct {
|
||||
a int
|
||||
b int
|
||||
}
|
||||
|
||||
gcdStep := func(state State) ReaderResult[E.Either[State, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[State, int]] {
|
||||
if state.b == 0 {
|
||||
return R.Of(E.Right[State](state.a))
|
||||
}
|
||||
return R.Of(E.Left[int](State{state.b, state.a % state.b}))
|
||||
}
|
||||
}
|
||||
|
||||
gcd := TailRec(gcdStep)
|
||||
result := gcd(State{48, 18})(context.Background())
|
||||
|
||||
assert.Equal(t, R.Of(6), result)
|
||||
}
|
||||
|
||||
// TestTailRecErrorPropagation tests that errors are properly propagated
|
||||
func TestTailRecErrorPropagation(t *testing.T) {
|
||||
expectedErr := errors.New("computation error")
|
||||
|
||||
errorStep := func(n int) ReaderResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[int, int]] {
|
||||
if n == 5 {
|
||||
return R.Left[E.Either[int, int]](expectedErr)
|
||||
}
|
||||
if n <= 0 {
|
||||
return R.Of(E.Right[int](n))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
computation := TailRec(errorStep)
|
||||
result := computation(10)(context.Background())
|
||||
|
||||
assert.True(t, R.IsLeft(result))
|
||||
_, err := R.Unwrap(result)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
}
|
||||
|
||||
// TestTailRecContextCancellationImmediate tests short circuit when context is already canceled
|
||||
func TestTailRecContextCancellationImmediate(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // Cancel immediately before execution
|
||||
|
||||
stepExecuted := false
|
||||
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[int, int]] {
|
||||
stepExecuted = true
|
||||
if n <= 0 {
|
||||
return R.Of(E.Right[int](n))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(10)(ctx)
|
||||
|
||||
// Should short circuit without executing any steps
|
||||
assert.False(t, stepExecuted, "Step should not be executed when context is already canceled")
|
||||
assert.True(t, R.IsLeft(result))
|
||||
_, err := R.Unwrap(result)
|
||||
assert.Equal(t, context.Canceled, err)
|
||||
}
|
||||
|
||||
// TestTailRecContextCancellationDuringExecution tests short circuit when context is canceled during execution
|
||||
func TestTailRecContextCancellationDuringExecution(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
executionCount := 0
|
||||
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[int, int]] {
|
||||
executionCount++
|
||||
// Cancel after 3 iterations
|
||||
if executionCount == 3 {
|
||||
cancel()
|
||||
}
|
||||
if n <= 0 {
|
||||
return R.Of(E.Right[int](n))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(100)(ctx)
|
||||
|
||||
// Should stop after cancellation
|
||||
assert.True(t, R.IsLeft(result))
|
||||
assert.LessOrEqual(t, executionCount, 4, "Should stop shortly after cancellation")
|
||||
_, err := R.Unwrap(result)
|
||||
assert.Equal(t, context.Canceled, err)
|
||||
}
|
||||
|
||||
// TestTailRecContextWithTimeout tests behavior with timeout context
|
||||
func TestTailRecContextWithTimeout(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
executionCount := 0
|
||||
slowStep := func(n int) ReaderResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[int, int]] {
|
||||
executionCount++
|
||||
// Simulate slow computation
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
if n <= 0 {
|
||||
return R.Of(E.Right[int](n))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
computation := TailRec(slowStep)
|
||||
result := computation(100)(ctx)
|
||||
|
||||
// Should timeout and return error
|
||||
assert.True(t, R.IsLeft(result))
|
||||
assert.Less(t, executionCount, 100, "Should not complete all iterations due to timeout")
|
||||
_, err := R.Unwrap(result)
|
||||
assert.Equal(t, context.DeadlineExceeded, err)
|
||||
}
|
||||
|
||||
// TestTailRecContextWithCause tests that context.Cause is properly returned
|
||||
func TestTailRecContextWithCause(t *testing.T) {
|
||||
customErr := errors.New("custom cancellation reason")
|
||||
ctx, cancel := context.WithCancelCause(context.Background())
|
||||
cancel(customErr)
|
||||
|
||||
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[int, int]] {
|
||||
if n <= 0 {
|
||||
return R.Of(E.Right[int](n))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(10)(ctx)
|
||||
|
||||
assert.True(t, R.IsLeft(result))
|
||||
_, err := R.Unwrap(result)
|
||||
assert.Equal(t, customErr, err)
|
||||
}
|
||||
|
||||
// TestTailRecContextCancellationMultipleIterations tests that cancellation is checked on each iteration
|
||||
func TestTailRecContextCancellationMultipleIterations(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
executionCount := 0
|
||||
maxExecutions := 5
|
||||
|
||||
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[int, int]] {
|
||||
executionCount++
|
||||
if executionCount == maxExecutions {
|
||||
cancel()
|
||||
}
|
||||
if n <= 0 {
|
||||
return R.Of(E.Right[int](n))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(1000)(ctx)
|
||||
|
||||
// Should detect cancellation on next iteration check
|
||||
assert.True(t, R.IsLeft(result))
|
||||
// Should stop within 1-2 iterations after cancellation
|
||||
assert.LessOrEqual(t, executionCount, maxExecutions+2)
|
||||
_, err := R.Unwrap(result)
|
||||
assert.Equal(t, context.Canceled, err)
|
||||
}
|
||||
|
||||
// TestTailRecContextNotCanceled tests normal execution when context is not canceled
|
||||
func TestTailRecContextNotCanceled(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
executionCount := 0
|
||||
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[int, int]] {
|
||||
executionCount++
|
||||
if n <= 0 {
|
||||
return R.Of(E.Right[int](n))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(10)(ctx)
|
||||
|
||||
assert.Equal(t, 11, executionCount) // 10, 9, 8, ..., 1, 0
|
||||
assert.Equal(t, R.Of(0), result)
|
||||
}
|
||||
|
||||
// TestTailRecPowerOfTwo tests computing power of 2
|
||||
func TestTailRecPowerOfTwo(t *testing.T) {
|
||||
type State struct {
|
||||
exponent int
|
||||
result int
|
||||
target int
|
||||
}
|
||||
|
||||
powerStep := func(state State) ReaderResult[E.Either[State, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[State, int]] {
|
||||
if state.exponent >= state.target {
|
||||
return R.Of(E.Right[State](state.result))
|
||||
}
|
||||
return R.Of(E.Left[int](State{state.exponent + 1, state.result * 2, state.target}))
|
||||
}
|
||||
}
|
||||
|
||||
power := TailRec(powerStep)
|
||||
result := power(State{0, 1, 10})(context.Background())
|
||||
|
||||
assert.Equal(t, R.Of(1024), result) // 2^10
|
||||
}
|
||||
|
||||
// TestTailRecFindInRange tests finding a value in a range
|
||||
func TestTailRecFindInRange(t *testing.T) {
|
||||
type State struct {
|
||||
current int
|
||||
max int
|
||||
target int
|
||||
}
|
||||
|
||||
findStep := func(state State) ReaderResult[E.Either[State, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[State, int]] {
|
||||
if state.current >= state.max {
|
||||
return R.Of(E.Right[State](-1)) // Not found
|
||||
}
|
||||
if state.current == state.target {
|
||||
return R.Of(E.Right[State](state.current)) // Found
|
||||
}
|
||||
return R.Of(E.Left[int](State{state.current + 1, state.max, state.target}))
|
||||
}
|
||||
}
|
||||
|
||||
find := TailRec(findStep)
|
||||
result := find(State{0, 100, 42})(context.Background())
|
||||
|
||||
assert.Equal(t, R.Of(42), result)
|
||||
}
|
||||
|
||||
// TestTailRecFindNotInRange tests finding a value not in range
|
||||
func TestTailRecFindNotInRange(t *testing.T) {
|
||||
type State struct {
|
||||
current int
|
||||
max int
|
||||
target int
|
||||
}
|
||||
|
||||
findStep := func(state State) ReaderResult[E.Either[State, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[State, int]] {
|
||||
if state.current >= state.max {
|
||||
return R.Of(E.Right[State](-1)) // Not found
|
||||
}
|
||||
if state.current == state.target {
|
||||
return R.Of(E.Right[State](state.current)) // Found
|
||||
}
|
||||
return R.Of(E.Left[int](State{state.current + 1, state.max, state.target}))
|
||||
}
|
||||
}
|
||||
|
||||
find := TailRec(findStep)
|
||||
result := find(State{0, 100, 200})(context.Background())
|
||||
|
||||
assert.Equal(t, R.Of(-1), result)
|
||||
}
|
||||
|
||||
// TestTailRecWithContextValue tests that context values are accessible
|
||||
func TestTailRecWithContextValue(t *testing.T) {
|
||||
type contextKey string
|
||||
const multiplierKey contextKey = "multiplier"
|
||||
|
||||
ctx := context.WithValue(context.Background(), multiplierKey, 3)
|
||||
|
||||
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[int, int]] {
|
||||
if n <= 0 {
|
||||
multiplier := ctx.Value(multiplierKey).(int)
|
||||
return R.Of(E.Right[int](n * multiplier))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(5)(ctx)
|
||||
|
||||
assert.Equal(t, R.Of(0), result) // 0 * 3 = 0
|
||||
}
|
||||
|
||||
// TestTailRecComplexState tests with complex state structure
|
||||
func TestTailRecComplexState(t *testing.T) {
|
||||
type ComplexState struct {
|
||||
counter int
|
||||
sum int
|
||||
product int
|
||||
completed bool
|
||||
}
|
||||
|
||||
complexStep := func(state ComplexState) ReaderResult[E.Either[ComplexState, string]] {
|
||||
return func(ctx context.Context) Result[E.Either[ComplexState, string]] {
|
||||
if state.counter <= 0 || state.completed {
|
||||
result := fmt.Sprintf("sum=%d, product=%d", state.sum, state.product)
|
||||
return R.Of(E.Right[ComplexState](result))
|
||||
}
|
||||
newState := ComplexState{
|
||||
counter: state.counter - 1,
|
||||
sum: state.sum + state.counter,
|
||||
product: state.product * state.counter,
|
||||
completed: state.counter == 1,
|
||||
}
|
||||
return R.Of(E.Left[string](newState))
|
||||
}
|
||||
}
|
||||
|
||||
computation := TailRec(complexStep)
|
||||
result := computation(ComplexState{5, 0, 1, false})(context.Background())
|
||||
|
||||
assert.Equal(t, R.Of("sum=15, product=120"), result)
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readereither"
|
||||
@@ -27,12 +28,14 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
Option[A any] = option.Option[A]
|
||||
Either[A any] = either.Either[error, A]
|
||||
Result[A any] = result.Result[A]
|
||||
Option[A any] = option.Option[A]
|
||||
Either[A any] = either.Either[error, A]
|
||||
Result[A any] = result.Result[A]
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
// ReaderResult is a specialization of the Reader monad for the typical golang scenario
|
||||
ReaderResult[A any] = readereither.ReaderEither[context.Context, error, A]
|
||||
|
||||
Kleisli[A, B any] = reader.Reader[A, ReaderResult[B]]
|
||||
Operator[A, B any] = Kleisli[ReaderResult[A], B]
|
||||
Kleisli[A, B any] = reader.Reader[A, ReaderResult[B]]
|
||||
Operator[A, B any] = Kleisli[ReaderResult[A], B]
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
)
|
||||
|
||||
9917
v2/coverage.out
9917
v2/coverage.out
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,21 @@
|
||||
// - Left represents an error or failure case (type E)
|
||||
// - Right represents a success case (type A)
|
||||
//
|
||||
// # Fantasy Land Specification
|
||||
//
|
||||
// This implementation corresponds to the Fantasy Land Either type:
|
||||
// https://github.com/fantasyland/fantasy-land#either
|
||||
//
|
||||
// Implemented Fantasy Land algebras:
|
||||
// - Functor: https://github.com/fantasyland/fantasy-land#functor
|
||||
// - Bifunctor: https://github.com/fantasyland/fantasy-land#bifunctor
|
||||
// - Apply: https://github.com/fantasyland/fantasy-land#apply
|
||||
// - Applicative: https://github.com/fantasyland/fantasy-land#applicative
|
||||
// - Chain: https://github.com/fantasyland/fantasy-land#chain
|
||||
// - Monad: https://github.com/fantasyland/fantasy-land#monad
|
||||
// - Alt: https://github.com/fantasyland/fantasy-land#alt
|
||||
// - Foldable: https://github.com/fantasyland/fantasy-land#foldable
|
||||
//
|
||||
// # Core Concepts
|
||||
//
|
||||
// The Either type is a discriminated union that can hold either a Left value (typically an error)
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
IO "github.com/IBM/fp-go/v2/io"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -120,10 +119,3 @@ func TestStringer(t *testing.T) {
|
||||
var s fmt.Stringer = &e
|
||||
assert.Equal(t, exp, s.String())
|
||||
}
|
||||
|
||||
func TestFromIO(t *testing.T) {
|
||||
f := IO.Of("abc")
|
||||
e := FromIO[error](f)
|
||||
|
||||
assert.Equal(t, Right[error]("abc"), e)
|
||||
}
|
||||
|
||||
@@ -17,11 +17,19 @@ package either
|
||||
|
||||
import (
|
||||
"log"
|
||||
"log/slog"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
L "github.com/IBM/fp-go/v2/logging"
|
||||
)
|
||||
|
||||
var (
|
||||
// slogError creates a slog.Attr with key "error" for logging error values
|
||||
slogError = F.Bind1st(slog.Any, "error")
|
||||
// slogValue creates a slog.Attr with key "value" for logging success values
|
||||
slogValue = F.Bind1st(slog.Any, "value")
|
||||
)
|
||||
|
||||
func _log[E, A any](left func(string, ...any), right func(string, ...any), prefix string) Operator[E, A, A] {
|
||||
return Fold(
|
||||
func(e E) Either[E, A] {
|
||||
@@ -62,3 +70,91 @@ func Logger[E, A any](loggers ...*log.Logger) func(string) Operator[E, A, A] {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ToSLogAttr converts an Either value to a structured logging attribute (slog.Attr).
|
||||
//
|
||||
// This function creates a converter that transforms Either values into slog.Attr for use
|
||||
// with Go's structured logging (log/slog). It maps:
|
||||
// - Left values to an "error" attribute
|
||||
// - Right values to a "value" attribute
|
||||
//
|
||||
// This is particularly useful when integrating Either-based error handling with structured
|
||||
// logging systems, allowing you to log both successful values and errors in a consistent,
|
||||
// structured format.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The Left (error) type of the Either
|
||||
// - A: The Right (success) type of the Either
|
||||
//
|
||||
// Returns:
|
||||
// - A function that converts Either[E, A] to slog.Attr
|
||||
//
|
||||
// Example with Left (error):
|
||||
//
|
||||
// converter := either.ToSLogAttr[error, int]()
|
||||
// leftValue := either.Left[int](errors.New("connection failed"))
|
||||
// attr := converter(leftValue)
|
||||
// // attr is: slog.Any("error", errors.New("connection failed"))
|
||||
//
|
||||
// logger.LogAttrs(ctx, slog.LevelError, "Operation failed", attr)
|
||||
// // Logs: {"level":"error","msg":"Operation failed","error":"connection failed"}
|
||||
//
|
||||
// Example with Right (success):
|
||||
//
|
||||
// converter := either.ToSLogAttr[error, User]()
|
||||
// rightValue := either.Right[error](User{ID: 123, Name: "Alice"})
|
||||
// attr := converter(rightValue)
|
||||
// // attr is: slog.Any("value", User{ID: 123, Name: "Alice"})
|
||||
//
|
||||
// logger.LogAttrs(ctx, slog.LevelInfo, "User fetched", attr)
|
||||
// // Logs: {"level":"info","msg":"User fetched","value":{"ID":123,"Name":"Alice"}}
|
||||
//
|
||||
// Example in a pipeline with structured logging:
|
||||
//
|
||||
// toAttr := either.ToSLogAttr[error, Data]()
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// fetchData(id),
|
||||
// either.Map(processData),
|
||||
// either.Map(validateData),
|
||||
// )
|
||||
//
|
||||
// attr := toAttr(result)
|
||||
// logger.LogAttrs(ctx, slog.LevelInfo, "Data processing complete", attr)
|
||||
// // Logs success: {"level":"info","msg":"Data processing complete","value":{...}}
|
||||
// // Or error: {"level":"info","msg":"Data processing complete","error":"validation failed"}
|
||||
//
|
||||
// Example with custom log levels based on Either:
|
||||
//
|
||||
// toAttr := either.ToSLogAttr[error, Response]()
|
||||
// result := callAPI(endpoint)
|
||||
//
|
||||
// level := either.Fold(
|
||||
// func(error) slog.Level { return slog.LevelError },
|
||||
// func(Response) slog.Level { return slog.LevelInfo },
|
||||
// )(result)
|
||||
//
|
||||
// logger.LogAttrs(ctx, level, "API call completed", toAttr(result))
|
||||
//
|
||||
// Use Cases:
|
||||
// - Structured logging: Convert Either results to structured log attributes
|
||||
// - Error tracking: Log errors with consistent "error" key in structured logs
|
||||
// - Success monitoring: Log successful values with consistent "value" key
|
||||
// - Observability: Integrate Either-based error handling with logging systems
|
||||
// - Debugging: Inspect Either values in logs with proper structure
|
||||
// - Metrics: Extract Either values for metrics collection in logging pipelines
|
||||
//
|
||||
// Note: The returned slog.Attr uses "error" for Left values and "value" for Right values.
|
||||
// These keys are consistent with common structured logging conventions.
|
||||
func ToSLogAttr[E, A any]() func(Either[E, A]) slog.Attr {
|
||||
return Fold(
|
||||
F.Flow2(
|
||||
F.ToAny[E],
|
||||
slogError,
|
||||
),
|
||||
F.Flow2(
|
||||
F.ToAny[A],
|
||||
slogValue,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,9 +16,12 @@
|
||||
package either
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -35,3 +38,139 @@ func TestLogger(t *testing.T) {
|
||||
|
||||
assert.Equal(t, r, res)
|
||||
}
|
||||
|
||||
func TestToSLogAttr_Left(t *testing.T) {
|
||||
// Test with Left (error) value
|
||||
converter := ToSLogAttr[error, int]()
|
||||
testErr := errors.New("test error")
|
||||
leftValue := Left[int](testErr)
|
||||
|
||||
attr := converter(leftValue)
|
||||
|
||||
// Verify the attribute has the correct key
|
||||
assert.Equal(t, "error", attr.Key)
|
||||
// Verify the attribute value is the error
|
||||
assert.Equal(t, testErr, attr.Value.Any())
|
||||
}
|
||||
|
||||
func TestToSLogAttr_Right(t *testing.T) {
|
||||
// Test with Right (success) value
|
||||
converter := ToSLogAttr[error, string]()
|
||||
rightValue := Right[error]("success value")
|
||||
|
||||
attr := converter(rightValue)
|
||||
|
||||
// Verify the attribute has the correct key
|
||||
assert.Equal(t, "value", attr.Key)
|
||||
// Verify the attribute value is the success value
|
||||
assert.Equal(t, "success value", attr.Value.Any())
|
||||
}
|
||||
|
||||
func TestToSLogAttr_LeftWithCustomType(t *testing.T) {
|
||||
// Test with custom error type
|
||||
type CustomError struct {
|
||||
Code int
|
||||
Message string
|
||||
}
|
||||
|
||||
converter := ToSLogAttr[CustomError, string]()
|
||||
customErr := CustomError{Code: 404, Message: "not found"}
|
||||
leftValue := Left[string](customErr)
|
||||
|
||||
attr := converter(leftValue)
|
||||
|
||||
assert.Equal(t, "error", attr.Key)
|
||||
assert.Equal(t, customErr, attr.Value.Any())
|
||||
}
|
||||
|
||||
func TestToSLogAttr_RightWithCustomType(t *testing.T) {
|
||||
// Test with custom success type
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
converter := ToSLogAttr[error, User]()
|
||||
user := User{ID: 123, Name: "Alice"}
|
||||
rightValue := Right[error](user)
|
||||
|
||||
attr := converter(rightValue)
|
||||
|
||||
assert.Equal(t, "value", attr.Key)
|
||||
assert.Equal(t, user, attr.Value.Any())
|
||||
}
|
||||
|
||||
func TestToSLogAttr_InPipeline(t *testing.T) {
|
||||
// Test ToSLogAttr in a functional pipeline
|
||||
converter := ToSLogAttr[error, int]()
|
||||
|
||||
// Test with successful pipeline
|
||||
successResult := F.Pipe2(
|
||||
Right[error](10),
|
||||
Map[error](N.Mul(2)),
|
||||
converter,
|
||||
)
|
||||
|
||||
assert.Equal(t, "value", successResult.Key)
|
||||
// slog.Any converts int to int64
|
||||
assert.Equal(t, int64(20), successResult.Value.Any())
|
||||
|
||||
// Test with failed pipeline
|
||||
testErr := errors.New("computation failed")
|
||||
failureResult := F.Pipe2(
|
||||
Left[int](testErr),
|
||||
Map[error](N.Mul(2)),
|
||||
converter,
|
||||
)
|
||||
|
||||
assert.Equal(t, "error", failureResult.Key)
|
||||
assert.Equal(t, testErr, failureResult.Value.Any())
|
||||
}
|
||||
|
||||
func TestToSLogAttr_WithNilError(t *testing.T) {
|
||||
// Test with nil error (edge case)
|
||||
converter := ToSLogAttr[error, string]()
|
||||
var nilErr error = nil
|
||||
leftValue := Left[string](nilErr)
|
||||
|
||||
attr := converter(leftValue)
|
||||
|
||||
assert.Equal(t, "error", attr.Key)
|
||||
assert.Nil(t, attr.Value.Any())
|
||||
}
|
||||
|
||||
func TestToSLogAttr_WithZeroValue(t *testing.T) {
|
||||
// Test with zero value of success type
|
||||
converter := ToSLogAttr[error, int]()
|
||||
rightValue := Right[error](0)
|
||||
|
||||
attr := converter(rightValue)
|
||||
|
||||
assert.Equal(t, "value", attr.Key)
|
||||
// slog.Any converts int to int64
|
||||
assert.Equal(t, int64(0), attr.Value.Any())
|
||||
}
|
||||
|
||||
func TestToSLogAttr_WithEmptyString(t *testing.T) {
|
||||
// Test with empty string as success value
|
||||
converter := ToSLogAttr[error, string]()
|
||||
rightValue := Right[error]("")
|
||||
|
||||
attr := converter(rightValue)
|
||||
|
||||
assert.Equal(t, "value", attr.Key)
|
||||
assert.Equal(t, "", attr.Value.Any())
|
||||
}
|
||||
|
||||
func TestToSLogAttr_AttributeKind(t *testing.T) {
|
||||
// Verify that the returned attribute has the correct Kind
|
||||
converter := ToSLogAttr[error, string]()
|
||||
|
||||
leftAttr := converter(Left[string](errors.New("error")))
|
||||
// Errors are stored as KindAny (which has value 0)
|
||||
assert.Equal(t, slog.KindAny, leftAttr.Value.Kind())
|
||||
|
||||
rightAttr := converter(Right[error]("value"))
|
||||
// Strings have KindString
|
||||
assert.Equal(t, slog.KindString, rightAttr.Value.Kind())
|
||||
}
|
||||
|
||||
34
v2/either/rec.go
Normal file
34
v2/either/rec.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package either
|
||||
|
||||
//go:inline
|
||||
func TailRec[E, A, B any](f Kleisli[E, A, Either[A, B]]) Kleisli[E, A, B] {
|
||||
return func(a A) Either[E, B] {
|
||||
current := f(a)
|
||||
for {
|
||||
rec, e := Unwrap(current)
|
||||
if IsLeft(current) {
|
||||
return Left[B](e)
|
||||
}
|
||||
b, a := Unwrap(rec)
|
||||
if IsRight(rec) {
|
||||
return Right[E](b)
|
||||
}
|
||||
current = f(a)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,18 @@
|
||||
// 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 endomorphism
|
||||
|
||||
import (
|
||||
@@ -5,6 +20,63 @@ import (
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
)
|
||||
|
||||
// FromSemigroup converts a semigroup into a Kleisli arrow for endomorphisms.
|
||||
//
|
||||
// This function takes a semigroup and returns a Kleisli arrow that, when given
|
||||
// a value of type A, produces an endomorphism that concatenates that value with
|
||||
// other values using the semigroup's Concat operation.
|
||||
//
|
||||
// The resulting Kleisli arrow has the signature: func(A) Endomorphism[A]
|
||||
// When called with a value 'x', it returns an endomorphism that concatenates
|
||||
// 'x' with its input using the semigroup's binary operation.
|
||||
//
|
||||
// # Data Last Principle
|
||||
//
|
||||
// FromSemigroup follows the "data last" principle by using function.Bind2of2,
|
||||
// which binds the second parameter of the semigroup's Concat operation.
|
||||
// This means that for a semigroup with Concat(a, b), calling FromSemigroup(s)(x)
|
||||
// creates an endomorphism that computes Concat(input, x), where the input data
|
||||
// comes first and the bound value 'x' comes last.
|
||||
//
|
||||
// For example, with string concatenation:
|
||||
// - Semigroup.Concat("Hello", "World") = "HelloWorld"
|
||||
// - FromSemigroup(semigroup)("World") creates: func(input) = Concat(input, "World")
|
||||
// - Applying it: endomorphism("Hello") = Concat("Hello", "World") = "HelloWorld"
|
||||
//
|
||||
// This is particularly useful for creating endomorphisms from associative operations
|
||||
// like string concatenation, number addition, list concatenation, etc.
|
||||
//
|
||||
// Parameters:
|
||||
// - s: A semigroup providing the Concat operation for type A
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that converts values of type A into endomorphisms
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/endomorphism"
|
||||
// "github.com/IBM/fp-go/v2/semigroup"
|
||||
// )
|
||||
//
|
||||
// // Create a semigroup for integer addition
|
||||
// addSemigroup := semigroup.MakeSemigroup(func(a, b int) int {
|
||||
// return a + b
|
||||
// })
|
||||
//
|
||||
// // Convert it to a Kleisli arrow
|
||||
// addKleisli := endomorphism.FromSemigroup(addSemigroup)
|
||||
//
|
||||
// // Use the Kleisli arrow to create an endomorphism that adds 5
|
||||
// // This follows "data last": the input data comes first, 5 comes last
|
||||
// addFive := addKleisli(5)
|
||||
//
|
||||
// // Apply the endomorphism: Concat(10, 5) = 10 + 5 = 15
|
||||
// result := addFive(10) // result is 15
|
||||
//
|
||||
// The function uses function.Bind2of2 to partially apply the semigroup's Concat
|
||||
// operation, effectively currying it to create the desired Kleisli arrow while
|
||||
// maintaining the "data last" principle.
|
||||
func FromSemigroup[A any](s S.Semigroup[A]) Kleisli[A] {
|
||||
return function.Bind2of2(s.Concat)
|
||||
}
|
||||
|
||||
439
v2/endomorphism/from_test.go
Normal file
439
v2/endomorphism/from_test.go
Normal file
@@ -0,0 +1,439 @@
|
||||
// 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 endomorphism
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestFromSemigroup tests the FromSemigroup function with various semigroups
|
||||
func TestFromSemigroup(t *testing.T) {
|
||||
t.Run("integer addition semigroup", func(t *testing.T) {
|
||||
// Create a semigroup for integer addition
|
||||
addSemigroup := S.MakeSemigroup(func(a, b int) int {
|
||||
return a + b
|
||||
})
|
||||
|
||||
// Convert to Kleisli arrow
|
||||
addKleisli := FromSemigroup(addSemigroup)
|
||||
|
||||
// Create an endomorphism that adds 5
|
||||
addFive := addKleisli(5)
|
||||
|
||||
// Test the endomorphism
|
||||
assert.Equal(t, 15, addFive(10), "addFive(10) should equal 15")
|
||||
assert.Equal(t, 5, addFive(0), "addFive(0) should equal 5")
|
||||
assert.Equal(t, -5, addFive(-10), "addFive(-10) should equal -5")
|
||||
})
|
||||
|
||||
t.Run("integer multiplication semigroup", func(t *testing.T) {
|
||||
// Create a semigroup for integer multiplication
|
||||
mulSemigroup := S.MakeSemigroup(func(a, b int) int {
|
||||
return a * b
|
||||
})
|
||||
|
||||
// Convert to Kleisli arrow
|
||||
mulKleisli := FromSemigroup(mulSemigroup)
|
||||
|
||||
// Create an endomorphism that multiplies by 3
|
||||
multiplyByThree := mulKleisli(3)
|
||||
|
||||
// Test the endomorphism
|
||||
assert.Equal(t, 15, multiplyByThree(5), "multiplyByThree(5) should equal 15")
|
||||
assert.Equal(t, 0, multiplyByThree(0), "multiplyByThree(0) should equal 0")
|
||||
assert.Equal(t, -9, multiplyByThree(-3), "multiplyByThree(-3) should equal -9")
|
||||
})
|
||||
|
||||
t.Run("string concatenation semigroup", func(t *testing.T) {
|
||||
// Create a semigroup for string concatenation
|
||||
concatSemigroup := S.MakeSemigroup(func(a, b string) string {
|
||||
return a + b
|
||||
})
|
||||
|
||||
// Convert to Kleisli arrow
|
||||
concatKleisli := FromSemigroup(concatSemigroup)
|
||||
|
||||
// Create an endomorphism that appends "Hello, " (input is on the left)
|
||||
appendHello := concatKleisli("Hello, ")
|
||||
|
||||
// Test the endomorphism - input is concatenated on the left, "Hello, " on the right
|
||||
assert.Equal(t, "WorldHello, ", appendHello("World"), "appendHello('World') should equal 'WorldHello, '")
|
||||
assert.Equal(t, "Hello, ", appendHello(""), "appendHello('') should equal 'Hello, '")
|
||||
assert.Equal(t, "GoHello, ", appendHello("Go"), "appendHello('Go') should equal 'GoHello, '")
|
||||
})
|
||||
|
||||
t.Run("slice concatenation semigroup", func(t *testing.T) {
|
||||
// Create a semigroup for slice concatenation
|
||||
sliceSemigroup := S.MakeSemigroup(func(a, b []int) []int {
|
||||
result := make([]int, len(a)+len(b))
|
||||
copy(result, a)
|
||||
copy(result[len(a):], b)
|
||||
return result
|
||||
})
|
||||
|
||||
// Convert to Kleisli arrow
|
||||
sliceKleisli := FromSemigroup(sliceSemigroup)
|
||||
|
||||
// Create an endomorphism that appends [1, 2] (input is on the left)
|
||||
appendOneTwo := sliceKleisli([]int{1, 2})
|
||||
|
||||
// Test the endomorphism - input is concatenated on the left, [1,2] on the right
|
||||
result1 := appendOneTwo([]int{3, 4, 5})
|
||||
assert.Equal(t, []int{3, 4, 5, 1, 2}, result1, "appendOneTwo([3,4,5]) should equal [3,4,5,1,2]")
|
||||
|
||||
result2 := appendOneTwo([]int{})
|
||||
assert.Equal(t, []int{1, 2}, result2, "appendOneTwo([]) should equal [1,2]")
|
||||
|
||||
result3 := appendOneTwo([]int{10})
|
||||
assert.Equal(t, []int{10, 1, 2}, result3, "appendOneTwo([10]) should equal [10,1,2]")
|
||||
})
|
||||
|
||||
t.Run("max semigroup", func(t *testing.T) {
|
||||
// Create a semigroup for max operation
|
||||
maxSemigroup := S.MakeSemigroup(func(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
})
|
||||
|
||||
// Convert to Kleisli arrow
|
||||
maxKleisli := FromSemigroup(maxSemigroup)
|
||||
|
||||
// Create an endomorphism that takes max with 10
|
||||
maxWithTen := maxKleisli(10)
|
||||
|
||||
// Test the endomorphism
|
||||
assert.Equal(t, 15, maxWithTen(15), "maxWithTen(15) should equal 15")
|
||||
assert.Equal(t, 10, maxWithTen(5), "maxWithTen(5) should equal 10")
|
||||
assert.Equal(t, 10, maxWithTen(10), "maxWithTen(10) should equal 10")
|
||||
assert.Equal(t, 10, maxWithTen(-5), "maxWithTen(-5) should equal 10")
|
||||
})
|
||||
|
||||
t.Run("min semigroup", func(t *testing.T) {
|
||||
// Create a semigroup for min operation
|
||||
minSemigroup := S.MakeSemigroup(func(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
})
|
||||
|
||||
// Convert to Kleisli arrow
|
||||
minKleisli := FromSemigroup(minSemigroup)
|
||||
|
||||
// Create an endomorphism that takes min with 10
|
||||
minWithTen := minKleisli(10)
|
||||
|
||||
// Test the endomorphism
|
||||
assert.Equal(t, 5, minWithTen(5), "minWithTen(5) should equal 5")
|
||||
assert.Equal(t, 10, minWithTen(15), "minWithTen(15) should equal 10")
|
||||
assert.Equal(t, 10, minWithTen(10), "minWithTen(10) should equal 10")
|
||||
assert.Equal(t, -5, minWithTen(-5), "minWithTen(-5) should equal -5")
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromSemigroupComposition tests that endomorphisms created from semigroups can be composed
|
||||
func TestFromSemigroupComposition(t *testing.T) {
|
||||
t.Run("compose addition endomorphisms", func(t *testing.T) {
|
||||
// Create a semigroup for integer addition
|
||||
addSemigroup := S.MakeSemigroup(func(a, b int) int {
|
||||
return a + b
|
||||
})
|
||||
addKleisli := FromSemigroup(addSemigroup)
|
||||
|
||||
// Create two endomorphisms
|
||||
addFive := addKleisli(5)
|
||||
addTen := addKleisli(10)
|
||||
|
||||
// Compose them (RIGHT-TO-LEFT execution)
|
||||
composed := MonadCompose(addFive, addTen)
|
||||
|
||||
// Test composition: addTen first, then addFive
|
||||
result := composed(3) // 3 + 10 = 13, then 13 + 5 = 18
|
||||
assert.Equal(t, 18, result, "composed addition should work correctly")
|
||||
})
|
||||
|
||||
t.Run("compose string endomorphisms", func(t *testing.T) {
|
||||
// Create a semigroup for string concatenation
|
||||
concatSemigroup := S.MakeSemigroup(func(a, b string) string {
|
||||
return a + b
|
||||
})
|
||||
concatKleisli := FromSemigroup(concatSemigroup)
|
||||
|
||||
// Create two endomorphisms
|
||||
appendHello := concatKleisli("Hello, ")
|
||||
appendExclamation := concatKleisli("!")
|
||||
|
||||
// Compose them (RIGHT-TO-LEFT execution)
|
||||
composed := MonadCompose(appendHello, appendExclamation)
|
||||
|
||||
// Test composition: appendExclamation first, then appendHello
|
||||
// "World" + "!" = "World!", then "World!" + "Hello, " = "World!Hello, "
|
||||
result := composed("World")
|
||||
assert.Equal(t, "World!Hello, ", result, "composed string operations should work correctly")
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromSemigroupWithMonoid tests using FromSemigroup-created endomorphisms with monoid operations
|
||||
func TestFromSemigroupWithMonoid(t *testing.T) {
|
||||
t.Run("monoid concat with addition endomorphisms", func(t *testing.T) {
|
||||
// Create a semigroup for integer addition
|
||||
addSemigroup := S.MakeSemigroup(func(a, b int) int {
|
||||
return a + b
|
||||
})
|
||||
addKleisli := FromSemigroup(addSemigroup)
|
||||
|
||||
// Create multiple endomorphisms
|
||||
addOne := addKleisli(1)
|
||||
addTwo := addKleisli(2)
|
||||
addThree := addKleisli(3)
|
||||
|
||||
// Use monoid to combine them
|
||||
monoid := Monoid[int]()
|
||||
combined := monoid.Concat(monoid.Concat(addOne, addTwo), addThree)
|
||||
|
||||
// Test: RIGHT-TO-LEFT execution: addThree, then addTwo, then addOne
|
||||
result := combined(10) // 10 + 3 = 13, 13 + 2 = 15, 15 + 1 = 16
|
||||
assert.Equal(t, 16, result, "monoid combination should work correctly")
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromSemigroupAssociativity tests that the semigroup associativity is preserved
|
||||
func TestFromSemigroupAssociativity(t *testing.T) {
|
||||
t.Run("addition associativity", func(t *testing.T) {
|
||||
// Create a semigroup for integer addition
|
||||
addSemigroup := S.MakeSemigroup(func(a, b int) int {
|
||||
return a + b
|
||||
})
|
||||
addKleisli := FromSemigroup(addSemigroup)
|
||||
|
||||
// Create three endomorphisms
|
||||
addTwo := addKleisli(2)
|
||||
addThree := addKleisli(3)
|
||||
addFive := addKleisli(5)
|
||||
|
||||
// Test associativity: (a . b) . c = a . (b . c)
|
||||
left := MonadCompose(MonadCompose(addTwo, addThree), addFive)
|
||||
right := MonadCompose(addTwo, MonadCompose(addThree, addFive))
|
||||
|
||||
testValue := 10
|
||||
assert.Equal(t, left(testValue), right(testValue), "composition should be associative")
|
||||
|
||||
// Both should equal: 10 + 5 + 3 + 2 = 20
|
||||
assert.Equal(t, 20, left(testValue), "left composition should equal 20")
|
||||
assert.Equal(t, 20, right(testValue), "right composition should equal 20")
|
||||
})
|
||||
|
||||
t.Run("string concatenation associativity", func(t *testing.T) {
|
||||
// Create a semigroup for string concatenation
|
||||
concatSemigroup := S.MakeSemigroup(func(a, b string) string {
|
||||
return a + b
|
||||
})
|
||||
concatKleisli := FromSemigroup(concatSemigroup)
|
||||
|
||||
// Create three endomorphisms
|
||||
appendA := concatKleisli("A")
|
||||
appendB := concatKleisli("B")
|
||||
appendC := concatKleisli("C")
|
||||
|
||||
// Test associativity: (a . b) . c = a . (b . c)
|
||||
left := MonadCompose(MonadCompose(appendA, appendB), appendC)
|
||||
right := MonadCompose(appendA, MonadCompose(appendB, appendC))
|
||||
|
||||
testValue := "X"
|
||||
assert.Equal(t, left(testValue), right(testValue), "string composition should be associative")
|
||||
|
||||
// Both should equal: "X" + "C" + "B" + "A" = "XCBA" (RIGHT-TO-LEFT composition)
|
||||
assert.Equal(t, "XCBA", left(testValue), "left composition should equal 'XCBA'")
|
||||
assert.Equal(t, "XCBA", right(testValue), "right composition should equal 'XCBA'")
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromSemigroupEdgeCases tests edge cases and boundary conditions
|
||||
func TestFromSemigroupEdgeCases(t *testing.T) {
|
||||
t.Run("zero values", func(t *testing.T) {
|
||||
// Test with addition and zero
|
||||
addSemigroup := S.MakeSemigroup(func(a, b int) int {
|
||||
return a + b
|
||||
})
|
||||
addKleisli := FromSemigroup(addSemigroup)
|
||||
|
||||
addZero := addKleisli(0)
|
||||
assert.Equal(t, 5, addZero(5), "adding zero should not change the value")
|
||||
assert.Equal(t, 0, addZero(0), "adding zero to zero should be zero")
|
||||
assert.Equal(t, -3, addZero(-3), "adding zero to negative should not change")
|
||||
})
|
||||
|
||||
t.Run("empty string", func(t *testing.T) {
|
||||
// Test with string concatenation and empty string
|
||||
concatSemigroup := S.MakeSemigroup(func(a, b string) string {
|
||||
return a + b
|
||||
})
|
||||
concatKleisli := FromSemigroup(concatSemigroup)
|
||||
|
||||
prependEmpty := concatKleisli("")
|
||||
assert.Equal(t, "hello", prependEmpty("hello"), "prepending empty string should not change")
|
||||
assert.Equal(t, "", prependEmpty(""), "prepending empty to empty should be empty")
|
||||
})
|
||||
|
||||
t.Run("empty slice", func(t *testing.T) {
|
||||
// Test with slice concatenation and empty slice
|
||||
sliceSemigroup := S.MakeSemigroup(func(a, b []int) []int {
|
||||
result := make([]int, len(a)+len(b))
|
||||
copy(result, a)
|
||||
copy(result[len(a):], b)
|
||||
return result
|
||||
})
|
||||
sliceKleisli := FromSemigroup(sliceSemigroup)
|
||||
|
||||
prependEmpty := sliceKleisli([]int{})
|
||||
result := prependEmpty([]int{1, 2, 3})
|
||||
assert.Equal(t, []int{1, 2, 3}, result, "prepending empty slice should not change")
|
||||
|
||||
emptyResult := prependEmpty([]int{})
|
||||
assert.Equal(t, []int{}, emptyResult, "prepending empty to empty should be empty")
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromSemigroupDataLastPrinciple explicitly tests that FromSemigroup follows the "data last" principle
|
||||
func TestFromSemigroupDataLastPrinciple(t *testing.T) {
|
||||
t.Run("data last with string concatenation", func(t *testing.T) {
|
||||
// Create a semigroup for string concatenation
|
||||
// Concat(a, b) = a + b
|
||||
concatSemigroup := S.MakeSemigroup(func(a, b string) string {
|
||||
return a + b
|
||||
})
|
||||
|
||||
// FromSemigroup uses Bind2of2, which binds the second parameter
|
||||
// So FromSemigroup(s)(x) creates: func(input) = Concat(input, x)
|
||||
// This is "data last" - the input data comes first, bound value comes last
|
||||
kleisli := FromSemigroup(concatSemigroup)
|
||||
|
||||
// Bind "World" as the second parameter
|
||||
appendWorld := kleisli("World")
|
||||
|
||||
// When we call appendWorld("Hello"), it computes Concat("Hello", "World")
|
||||
// The input "Hello" is the first parameter (data), "World" is the second (bound value)
|
||||
result := appendWorld("Hello")
|
||||
assert.Equal(t, "HelloWorld", result, "Data last: Concat(input='Hello', bound='World') = 'HelloWorld'")
|
||||
|
||||
// Verify with different input
|
||||
result2 := appendWorld("Goodbye")
|
||||
assert.Equal(t, "GoodbyeWorld", result2, "Data last: Concat(input='Goodbye', bound='World') = 'GoodbyeWorld'")
|
||||
})
|
||||
|
||||
t.Run("data last with integer addition", func(t *testing.T) {
|
||||
// Create a semigroup for integer addition
|
||||
// Concat(a, b) = a + b
|
||||
addSemigroup := S.MakeSemigroup(func(a, b int) int {
|
||||
return a + b
|
||||
})
|
||||
|
||||
// FromSemigroup binds the second parameter
|
||||
// So FromSemigroup(s)(5) creates: func(input) = Concat(input, 5) = input + 5
|
||||
kleisli := FromSemigroup(addSemigroup)
|
||||
|
||||
// Bind 5 as the second parameter
|
||||
addFive := kleisli(5)
|
||||
|
||||
// When we call addFive(10), it computes Concat(10, 5) = 10 + 5 = 15
|
||||
// The input 10 is the first parameter (data), 5 is the second (bound value)
|
||||
result := addFive(10)
|
||||
assert.Equal(t, 15, result, "Data last: Concat(input=10, bound=5) = 15")
|
||||
})
|
||||
|
||||
t.Run("data last with non-commutative operation", func(t *testing.T) {
|
||||
// Create a semigroup for a non-commutative operation to clearly show order
|
||||
// Concat(a, b) = a - b (subtraction is not commutative)
|
||||
subSemigroup := S.MakeSemigroup(func(a, b int) int {
|
||||
return a - b
|
||||
})
|
||||
|
||||
// FromSemigroup binds the second parameter
|
||||
// So FromSemigroup(s)(5) creates: func(input) = Concat(input, 5) = input - 5
|
||||
kleisli := FromSemigroup(subSemigroup)
|
||||
|
||||
// Bind 5 as the second parameter
|
||||
subtractFive := kleisli(5)
|
||||
|
||||
// When we call subtractFive(10), it computes Concat(10, 5) = 10 - 5 = 5
|
||||
// The input 10 is the first parameter (data), 5 is the second (bound value)
|
||||
result := subtractFive(10)
|
||||
assert.Equal(t, 5, result, "Data last: Concat(input=10, bound=5) = 10 - 5 = 5")
|
||||
|
||||
// If it were "data first" (binding first parameter), we would get:
|
||||
// Concat(5, 10) = 5 - 10 = -5, which is NOT what we get
|
||||
assert.NotEqual(t, -5, result, "Not data first: result is NOT Concat(bound=5, input=10) = 5 - 10 = -5")
|
||||
})
|
||||
|
||||
t.Run("data last with list concatenation", func(t *testing.T) {
|
||||
// Create a semigroup for list concatenation
|
||||
// Concat(a, b) = a ++ b
|
||||
listSemigroup := S.MakeSemigroup(func(a, b []int) []int {
|
||||
result := make([]int, len(a)+len(b))
|
||||
copy(result, a)
|
||||
copy(result[len(a):], b)
|
||||
return result
|
||||
})
|
||||
|
||||
// FromSemigroup binds the second parameter
|
||||
// So FromSemigroup(s)([3,4]) creates: func(input) = Concat(input, [3,4])
|
||||
kleisli := FromSemigroup(listSemigroup)
|
||||
|
||||
// Bind [3, 4] as the second parameter
|
||||
appendThreeFour := kleisli([]int{3, 4})
|
||||
|
||||
// When we call appendThreeFour([1,2]), it computes Concat([1,2], [3,4]) = [1,2,3,4]
|
||||
// The input [1,2] is the first parameter (data), [3,4] is the second (bound value)
|
||||
result := appendThreeFour([]int{1, 2})
|
||||
assert.Equal(t, []int{1, 2, 3, 4}, result, "Data last: Concat(input=[1,2], bound=[3,4]) = [1,2,3,4]")
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkFromSemigroup benchmarks the FromSemigroup function
|
||||
func BenchmarkFromSemigroup(b *testing.B) {
|
||||
addSemigroup := S.MakeSemigroup(func(a, b int) int {
|
||||
return a + b
|
||||
})
|
||||
addKleisli := FromSemigroup(addSemigroup)
|
||||
addFive := addKleisli(5)
|
||||
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = addFive(10)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkFromSemigroupComposition benchmarks composed endomorphisms from semigroups
|
||||
func BenchmarkFromSemigroupComposition(b *testing.B) {
|
||||
addSemigroup := S.MakeSemigroup(func(a, b int) int {
|
||||
return a + b
|
||||
})
|
||||
addKleisli := FromSemigroup(addSemigroup)
|
||||
|
||||
addFive := addKleisli(5)
|
||||
addTen := addKleisli(10)
|
||||
composed := MonadCompose(addFive, addTen)
|
||||
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = composed(3)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -117,9 +117,13 @@ func Nullary2[F1 ~func() T1, F2 ~func(T1) T2, T1, T2 any](f1 F1, f2 F2) func() T
|
||||
|
||||
// Curry2 takes a function with 2 parameters and returns a cascade of functions each taking only one parameter.
|
||||
// The inverse function is [Uncurry2]
|
||||
//go:inline
|
||||
func Curry2[FCT ~func(T0, T1) T2, T0, T1, T2 any](f FCT) func(T0) func(T1) T2 {
|
||||
//go:inline
|
||||
return func(t0 T0) func(t1 T1) T2 {
|
||||
//go:inline
|
||||
return func(t1 T1) T2 {
|
||||
//go:inline
|
||||
return f(t0, t1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ package function
|
||||
// Example:
|
||||
//
|
||||
// isPositive := func(n int) bool { return n > 0 }
|
||||
// double := func(n int) int { return n * 2 }
|
||||
// double := N.Mul(2)
|
||||
// negate := func(n int) int { return -n }
|
||||
//
|
||||
// transform := Ternary(isPositive, double, negate)
|
||||
|
||||
@@ -16,6 +16,18 @@
|
||||
/*
|
||||
Package identity implements the Identity monad, the simplest possible monad.
|
||||
|
||||
# Fantasy Land Specification
|
||||
|
||||
This implementation corresponds to the Fantasy Land Identity type:
|
||||
https://github.com/fantasyland/fantasy-land
|
||||
|
||||
Implemented Fantasy Land algebras:
|
||||
- Functor: https://github.com/fantasyland/fantasy-land#functor
|
||||
- Apply: https://github.com/fantasyland/fantasy-land#apply
|
||||
- Applicative: https://github.com/fantasyland/fantasy-land#applicative
|
||||
- Chain: https://github.com/fantasyland/fantasy-land#chain
|
||||
- Monad: https://github.com/fantasyland/fantasy-land#monad
|
||||
|
||||
# Overview
|
||||
|
||||
The Identity monad is a trivial monad that simply wraps a value without adding
|
||||
@@ -107,8 +119,8 @@ Chain for sequential composition:
|
||||
// Chain multiple operations
|
||||
result := F.Pipe2(
|
||||
10,
|
||||
identity.Chain(func(n int) int { return n * 2 }),
|
||||
identity.Chain(func(n int) int { return n + 5 }),
|
||||
identity.Chain(N.Mul(2)),
|
||||
identity.Chain(N.Add(5)),
|
||||
)
|
||||
// result is 25
|
||||
|
||||
@@ -177,8 +189,8 @@ Convert tuples of Identity values:
|
||||
// Traverse with transformation
|
||||
tuple := T.MakeTuple2(1, 2)
|
||||
result := identity.TraverseTuple2(
|
||||
func(n int) int { return n * 2 },
|
||||
func(n int) int { return n * 3 },
|
||||
N.Mul(2),
|
||||
N.Mul(3),
|
||||
)(tuple)
|
||||
// result is T.Tuple2[int, int]{2, 6}
|
||||
|
||||
@@ -211,7 +223,7 @@ Example of generic code:
|
||||
) M {
|
||||
return F.Pipe2(
|
||||
monad.Of(value),
|
||||
monad.Map(func(n int) int { return n * 2 }),
|
||||
monad.Map(N.Mul(2)),
|
||||
monad.Map(func(n int) string { return fmt.Sprintf("%d", n) }),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,10 +17,13 @@ package identity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -51,17 +54,15 @@ func TestMap(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("transforms string", func(t *testing.T) {
|
||||
result := F.Pipe1("hello", Map(func(s string) int {
|
||||
return len(s)
|
||||
}))
|
||||
result := F.Pipe1("hello", Map(S.Size))
|
||||
assert.Equal(t, 5, result)
|
||||
})
|
||||
|
||||
t.Run("chains multiple maps", func(t *testing.T) {
|
||||
result := F.Pipe2(
|
||||
5,
|
||||
Map(func(n int) int { return n * 2 }),
|
||||
Map(func(n int) int { return n + 3 }),
|
||||
Map(N.Mul(2)),
|
||||
Map(N.Add(3)),
|
||||
)
|
||||
assert.Equal(t, 13, result)
|
||||
})
|
||||
@@ -69,14 +70,12 @@ func TestMap(t *testing.T) {
|
||||
|
||||
func TestMonadMap(t *testing.T) {
|
||||
t.Run("transforms value", func(t *testing.T) {
|
||||
result := MonadMap(10, func(n int) int { return n * 3 })
|
||||
result := MonadMap(10, N.Mul(3))
|
||||
assert.Equal(t, 30, result)
|
||||
})
|
||||
|
||||
t.Run("changes type", func(t *testing.T) {
|
||||
result := MonadMap(42, func(n int) string {
|
||||
return fmt.Sprintf("Number: %d", n)
|
||||
})
|
||||
result := MonadMap(42, S.Format[int]("Number: %d"))
|
||||
assert.Equal(t, "Number: 42", result)
|
||||
})
|
||||
}
|
||||
@@ -109,23 +108,21 @@ func TestChain(t *testing.T) {
|
||||
t.Run("chains multiple operations", func(t *testing.T) {
|
||||
result := F.Pipe2(
|
||||
10,
|
||||
Chain(func(n int) int { return n * 2 }),
|
||||
Chain(func(n int) int { return n + 5 }),
|
||||
Chain(N.Mul(2)),
|
||||
Chain(N.Add(5)),
|
||||
)
|
||||
assert.Equal(t, 25, result)
|
||||
})
|
||||
|
||||
t.Run("changes type", func(t *testing.T) {
|
||||
result := F.Pipe1(5, Chain(func(n int) string {
|
||||
return fmt.Sprintf("Value: %d", n)
|
||||
}))
|
||||
result := F.Pipe1(5, Chain(S.Format[int]("Value: %d")))
|
||||
assert.Equal(t, "Value: 5", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadChain(t *testing.T) {
|
||||
t.Run("chains computation", func(t *testing.T) {
|
||||
result := MonadChain(7, func(n int) int { return n * 7 })
|
||||
result := MonadChain(7, N.Mul(7))
|
||||
assert.Equal(t, 49, result)
|
||||
})
|
||||
}
|
||||
@@ -148,7 +145,7 @@ func TestChainFirst(t *testing.T) {
|
||||
result := F.Pipe2(
|
||||
10,
|
||||
ChainFirst(func(n int) string { return "ignored" }),
|
||||
Map(func(n int) int { return n * 2 }),
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
assert.Equal(t, 20, result)
|
||||
})
|
||||
@@ -156,9 +153,7 @@ func TestChainFirst(t *testing.T) {
|
||||
|
||||
func TestMonadChainFirst(t *testing.T) {
|
||||
t.Run("keeps original value", func(t *testing.T) {
|
||||
result := MonadChainFirst(100, func(n int) string {
|
||||
return fmt.Sprintf("%d", n)
|
||||
})
|
||||
result := MonadChainFirst(100, strconv.Itoa)
|
||||
assert.Equal(t, 100, result)
|
||||
})
|
||||
}
|
||||
@@ -170,17 +165,13 @@ func TestAp(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("applies curried function", func(t *testing.T) {
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
add := N.Add[int]
|
||||
result := F.Pipe1(add(10), Ap[int](5))
|
||||
assert.Equal(t, 15, result)
|
||||
})
|
||||
|
||||
t.Run("changes type", func(t *testing.T) {
|
||||
toString := func(n int) string {
|
||||
return fmt.Sprintf("Number: %d", n)
|
||||
}
|
||||
toString := S.Format[int]("Number: %d")
|
||||
result := F.Pipe1(toString, Ap[string](42))
|
||||
assert.Equal(t, "Number: 42", result)
|
||||
})
|
||||
@@ -188,22 +179,22 @@ func TestAp(t *testing.T) {
|
||||
|
||||
func TestMonadAp(t *testing.T) {
|
||||
t.Run("applies function to value", func(t *testing.T) {
|
||||
result := MonadAp(func(n int) int { return n * 3 }, 7)
|
||||
result := MonadAp(N.Mul(3), 7)
|
||||
assert.Equal(t, 21, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFlap(t *testing.T) {
|
||||
t.Run("flips application", func(t *testing.T) {
|
||||
double := func(n int) int { return n * 2 }
|
||||
double := N.Mul(2)
|
||||
result := F.Pipe1(double, Flap[int](5))
|
||||
assert.Equal(t, 10, result)
|
||||
})
|
||||
|
||||
t.Run("with multiple functions", func(t *testing.T) {
|
||||
funcs := []func(int) int{
|
||||
func(n int) int { return n * 2 },
|
||||
func(n int) int { return n + 10 },
|
||||
N.Mul(2),
|
||||
N.Add(10),
|
||||
func(n int) int { return n * n },
|
||||
}
|
||||
|
||||
@@ -218,9 +209,7 @@ func TestFlap(t *testing.T) {
|
||||
|
||||
func TestMonadFlap(t *testing.T) {
|
||||
t.Run("applies value to function", func(t *testing.T) {
|
||||
result := MonadFlap(func(n int) string {
|
||||
return fmt.Sprintf("Value: %d", n)
|
||||
}, 42)
|
||||
result := MonadFlap(S.Format[int]("Value: %d"), 42)
|
||||
assert.Equal(t, "Value: 42", result)
|
||||
})
|
||||
}
|
||||
@@ -391,8 +380,8 @@ func TestTraverseTuple(t *testing.T) {
|
||||
t.Run("TraverseTuple2", func(t *testing.T) {
|
||||
tuple := T.MakeTuple2(1, 2)
|
||||
result := TraverseTuple2(
|
||||
func(n int) int { return n * 2 },
|
||||
func(n int) int { return n * 3 },
|
||||
N.Mul(2),
|
||||
N.Mul(3),
|
||||
)(tuple)
|
||||
assert.Equal(t, T.MakeTuple2(2, 6), result)
|
||||
})
|
||||
@@ -400,7 +389,7 @@ func TestTraverseTuple(t *testing.T) {
|
||||
t.Run("TraverseTuple3", func(t *testing.T) {
|
||||
tuple := T.MakeTuple3(1, 2, 3)
|
||||
result := TraverseTuple3(
|
||||
func(n int) int { return n + 10 },
|
||||
N.Add(10),
|
||||
func(n int) int { return n + 20 },
|
||||
func(n int) int { return n + 30 },
|
||||
)(tuple)
|
||||
@@ -426,15 +415,11 @@ func TestMonad(t *testing.T) {
|
||||
assert.Equal(t, 42, value)
|
||||
|
||||
// Test Map
|
||||
mapped := m.Map(func(n int) string {
|
||||
return fmt.Sprintf("Number: %d", n)
|
||||
})(value)
|
||||
mapped := m.Map(S.Format[int]("Number: %d"))(value)
|
||||
assert.Equal(t, "Number: 42", mapped)
|
||||
|
||||
// Test Chain
|
||||
chained := m.Chain(func(n int) string {
|
||||
return fmt.Sprintf("Value: %d", n)
|
||||
})(value)
|
||||
chained := m.Chain(S.Format[int]("Value: %d"))(value)
|
||||
assert.Equal(t, "Value: 42", chained)
|
||||
|
||||
// Test Ap
|
||||
@@ -450,7 +435,7 @@ func TestMonadLaws(t *testing.T) {
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
// Of(a).Chain(f) === f(a)
|
||||
a := 42
|
||||
f := func(n int) int { return n * 2 }
|
||||
f := N.Mul(2)
|
||||
|
||||
left := F.Pipe1(Of(a), Chain(f))
|
||||
right := f(a)
|
||||
@@ -470,8 +455,8 @@ func TestMonadLaws(t *testing.T) {
|
||||
t.Run("associativity", func(t *testing.T) {
|
||||
// m.Chain(f).Chain(g) === m.Chain(x => f(x).Chain(g))
|
||||
m := 5
|
||||
f := func(n int) int { return n * 2 }
|
||||
g := func(n int) int { return n + 10 }
|
||||
f := N.Mul(2)
|
||||
g := N.Add(10)
|
||||
|
||||
left := F.Pipe2(m, Chain(f), Chain(g))
|
||||
right := F.Pipe1(m, Chain(func(x int) int {
|
||||
@@ -496,8 +481,8 @@ func TestFunctorLaws(t *testing.T) {
|
||||
t.Run("composition", func(t *testing.T) {
|
||||
// Map(f).Map(g) === Map(g ∘ f)
|
||||
value := 5
|
||||
f := func(n int) int { return n * 2 }
|
||||
g := func(n int) int { return n + 10 }
|
||||
f := N.Mul(2)
|
||||
g := N.Add(10)
|
||||
|
||||
left := F.Pipe2(value, Map(f), Map(g))
|
||||
right := F.Pipe1(value, Map(F.Flow2(f, g)))
|
||||
@@ -541,7 +526,7 @@ func TestTraverseTuple4(t *testing.T) {
|
||||
t.Run("traverses tuple4", func(t *testing.T) {
|
||||
tuple := T.MakeTuple4(1, 2, 3, 4)
|
||||
result := TraverseTuple4(
|
||||
func(n int) int { return n + 10 },
|
||||
N.Add(10),
|
||||
func(n int) int { return n + 20 },
|
||||
func(n int) int { return n + 30 },
|
||||
func(n int) int { return n + 40 },
|
||||
@@ -570,8 +555,8 @@ func TestTraverseTuple5(t *testing.T) {
|
||||
tuple := T.MakeTuple5(1, 2, 3, 4, 5)
|
||||
result := TraverseTuple5(
|
||||
func(n int) int { return n * 1 },
|
||||
func(n int) int { return n * 2 },
|
||||
func(n int) int { return n * 3 },
|
||||
N.Mul(2),
|
||||
N.Mul(3),
|
||||
func(n int) int { return n * 4 },
|
||||
func(n int) int { return n * 5 },
|
||||
)(tuple)
|
||||
@@ -598,11 +583,11 @@ func TestTraverseTuple6(t *testing.T) {
|
||||
t.Run("traverses tuple6", func(t *testing.T) {
|
||||
tuple := T.MakeTuple6(1, 2, 3, 4, 5, 6)
|
||||
result := TraverseTuple6(
|
||||
func(n int) int { return n + 1 },
|
||||
N.Add(1),
|
||||
func(n int) int { return n + 2 },
|
||||
func(n int) int { return n + 3 },
|
||||
N.Add(3),
|
||||
func(n int) int { return n + 4 },
|
||||
func(n int) int { return n + 5 },
|
||||
N.Add(5),
|
||||
func(n int) int { return n + 6 },
|
||||
)(tuple)
|
||||
assert.Equal(t, T.MakeTuple6(2, 4, 6, 8, 10, 12), result)
|
||||
@@ -691,15 +676,15 @@ func TestTraverseTuple9(t *testing.T) {
|
||||
t.Run("traverses tuple9", func(t *testing.T) {
|
||||
tuple := T.MakeTuple9(1, 2, 3, 4, 5, 6, 7, 8, 9)
|
||||
result := TraverseTuple9(
|
||||
func(n int) int { return n + 1 },
|
||||
func(n int) int { return n + 1 },
|
||||
func(n int) int { return n + 1 },
|
||||
func(n int) int { return n + 1 },
|
||||
func(n int) int { return n + 1 },
|
||||
func(n int) int { return n + 1 },
|
||||
func(n int) int { return n + 1 },
|
||||
func(n int) int { return n + 1 },
|
||||
func(n int) int { return n + 1 },
|
||||
N.Add(1),
|
||||
N.Add(1),
|
||||
N.Add(1),
|
||||
N.Add(1),
|
||||
N.Add(1),
|
||||
N.Add(1),
|
||||
N.Add(1),
|
||||
N.Add(1),
|
||||
N.Add(1),
|
||||
)(tuple)
|
||||
assert.Equal(t, T.MakeTuple9(2, 3, 4, 5, 6, 7, 8, 9, 10), result)
|
||||
})
|
||||
@@ -724,16 +709,16 @@ func TestTraverseTuple10(t *testing.T) {
|
||||
t.Run("traverses tuple10", func(t *testing.T) {
|
||||
tuple := T.MakeTuple10(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
result := TraverseTuple10(
|
||||
func(n int) int { return n * 2 },
|
||||
func(n int) int { return n * 2 },
|
||||
func(n int) int { return n * 2 },
|
||||
func(n int) int { return n * 2 },
|
||||
func(n int) int { return n * 2 },
|
||||
func(n int) int { return n * 2 },
|
||||
func(n int) int { return n * 2 },
|
||||
func(n int) int { return n * 2 },
|
||||
func(n int) int { return n * 2 },
|
||||
func(n int) int { return n * 2 },
|
||||
N.Mul(2),
|
||||
N.Mul(2),
|
||||
N.Mul(2),
|
||||
N.Mul(2),
|
||||
N.Mul(2),
|
||||
N.Mul(2),
|
||||
N.Mul(2),
|
||||
N.Mul(2),
|
||||
N.Mul(2),
|
||||
N.Mul(2),
|
||||
)(tuple)
|
||||
assert.Equal(t, T.MakeTuple10(2, 4, 6, 8, 10, 12, 14, 16, 18, 20), result)
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
60
v2/internal/readert/flip.go
Normal file
60
v2/internal/readert/flip.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package readert
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/identity"
|
||||
)
|
||||
|
||||
func Sequence[
|
||||
HKTR2HKTR1A ~func(R2) HKTR1HKTA,
|
||||
R1, R2, HKTR1HKTA, HKTA any](
|
||||
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(identity.Ap[HKTA](r1))(ma(r2))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func SequenceReader[
|
||||
HKTR2HKTR1A ~func(R2) HKTR1HKTA,
|
||||
R1, R2, A, HKTR1HKTA, HKTA any](
|
||||
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(identity.Ap[A](r1))(ma(r2))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Traverse[
|
||||
HKTR2A ~func(R2) HKTA,
|
||||
HKTR1B ~func(R1) HKTB,
|
||||
R1, R2, A, HKTR1HKTB, HKTA, HKTB any](
|
||||
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 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[
|
||||
HKTR2A ~func(R2) HKTA,
|
||||
HKTR1B ~func(R1) B,
|
||||
R1, R2, A, B, HKTR1HKTB, HKTA, HKTB any](
|
||||
mmap1 func(func(A) HKTR1B) func(HKTA) HKTR1HKTB,
|
||||
mmap2 func(func(func(R1) B) B) func(HKTR1HKTB) HKTB,
|
||||
f func(A) HKTR1B,
|
||||
) func(HKTR2A) func(R1) func(R2) HKTB {
|
||||
return function.Flow2(
|
||||
function.Bind1of2(function.Bind2of3(function.Flow3[HKTR2A, func(HKTA) HKTR1HKTB, func(HKTR1HKTB) HKTB])(mmap1(f))),
|
||||
function.Bind12of3(function.Flow3[func(fa R1) identity.Operator[func(R1) B, B], func(func(func(R1) B) B) func(HKTR1HKTB) HKTB, func(func(HKTR1HKTB) HKTB) func(R2) HKTB])(identity.Ap[B, R1], mmap2),
|
||||
)
|
||||
}
|
||||
127
v2/io/consumer.go
Normal file
127
v2/io/consumer.go
Normal file
@@ -0,0 +1,127 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package io
|
||||
|
||||
// ChainConsumer converts a Consumer into an IO operator that executes the consumer
|
||||
// as a side effect and returns an empty struct.
|
||||
//
|
||||
// This function bridges the gap between pure consumers (functions that consume values
|
||||
// without returning anything) and the IO monad. It takes a Consumer[A] and returns
|
||||
// an Operator that:
|
||||
// 1. Executes the source IO[A] to get a value
|
||||
// 2. Passes that value to the consumer for side effects
|
||||
// 3. Returns IO[struct{}] to maintain the monadic chain
|
||||
//
|
||||
// The returned IO[struct{}] allows the operation to be composed with other IO operations
|
||||
// while discarding the consumed value. This is useful for operations like logging,
|
||||
// printing, or updating external state within an IO pipeline.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of value consumed by the consumer
|
||||
//
|
||||
// Parameters:
|
||||
// - c: A Consumer[A] that performs side effects on values of type A
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator[A, struct{}] that executes the consumer and returns an empty struct
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a consumer that logs values
|
||||
// logger := func(x int) {
|
||||
// fmt.Printf("Value: %d\n", x)
|
||||
// }
|
||||
//
|
||||
// // Convert it to an IO operator
|
||||
// logOp := io.ChainConsumer(logger)
|
||||
//
|
||||
// // Use it in an IO pipeline
|
||||
// result := F.Pipe2(
|
||||
// io.Of(42),
|
||||
// logOp, // Logs "Value: 42"
|
||||
// io.Map(func(struct{}) string { return "done" }),
|
||||
// )
|
||||
// result() // Returns "done" after logging
|
||||
//
|
||||
// // Another example with multiple operations
|
||||
// var values []int
|
||||
// collector := func(x int) {
|
||||
// values = append(values, x)
|
||||
// }
|
||||
//
|
||||
// pipeline := F.Pipe2(
|
||||
// io.Of(100),
|
||||
// io.ChainConsumer(collector), // Collects the value
|
||||
// io.Map(func(struct{}) int { return len(values) }),
|
||||
// )
|
||||
// count := pipeline() // Returns 1, values contains [100]
|
||||
func ChainConsumer[A any](c Consumer[A]) Operator[A, struct{}] {
|
||||
return Chain(FromConsumerK(c))
|
||||
}
|
||||
|
||||
// FromConsumerK converts a Consumer into a Kleisli arrow that wraps the consumer
|
||||
// in an IO context.
|
||||
//
|
||||
// This function lifts a Consumer[A] (a function that consumes a value and performs
|
||||
// side effects) into a Kleisli[A, struct{}] (a function that takes a value and returns
|
||||
// an IO computation that performs the side effect and returns an empty struct).
|
||||
//
|
||||
// The resulting Kleisli arrow can be used with Chain and other monadic operations
|
||||
// to integrate consumers into IO pipelines. This is a lower-level function compared
|
||||
// to ChainConsumer, which directly returns an Operator.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of value consumed by the consumer
|
||||
//
|
||||
// Parameters:
|
||||
// - c: A Consumer[A] that performs side effects on values of type A
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli[A, struct{}] that wraps the consumer in an IO context
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a consumer
|
||||
// logger := func(x int) {
|
||||
// fmt.Printf("Logging: %d\n", x)
|
||||
// }
|
||||
//
|
||||
// // Convert to Kleisli arrow
|
||||
// logKleisli := io.FromConsumerK(logger)
|
||||
//
|
||||
// // Use with Chain
|
||||
// result := F.Pipe2(
|
||||
// io.Of(42),
|
||||
// io.Chain(logKleisli), // Logs "Logging: 42"
|
||||
// io.Map(func(struct{}) string { return "completed" }),
|
||||
// )
|
||||
// result() // Returns "completed"
|
||||
//
|
||||
// // Can also be used to build more complex operations
|
||||
// logAndCount := func(x int) io.IO[int] {
|
||||
// return F.Pipe2(
|
||||
// logKleisli(x),
|
||||
// io.Map(func(struct{}) int { return 1 }),
|
||||
// )
|
||||
// }
|
||||
func FromConsumerK[A any](c Consumer[A]) Kleisli[A, struct{}] {
|
||||
return func(a A) IO[struct{}] {
|
||||
return func() struct{} {
|
||||
c(a)
|
||||
return struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
265
v2/io/consumer_test.go
Normal file
265
v2/io/consumer_test.go
Normal file
@@ -0,0 +1,265 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package io
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestChainConsumer(t *testing.T) {
|
||||
t.Run("executes consumer with IO value", func(t *testing.T) {
|
||||
var captured int
|
||||
consumer := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
result := F.Pipe1(
|
||||
Of(42),
|
||||
ChainConsumer(consumer),
|
||||
)
|
||||
|
||||
// Execute the IO
|
||||
result()
|
||||
|
||||
// Verify the consumer was called with the correct value
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
|
||||
t.Run("returns empty struct", func(t *testing.T) {
|
||||
consumer := func(x int) {
|
||||
// no-op consumer
|
||||
}
|
||||
|
||||
result := F.Pipe1(
|
||||
Of(100),
|
||||
ChainConsumer(consumer),
|
||||
)
|
||||
|
||||
// Execute and verify return type
|
||||
output := result()
|
||||
assert.Equal(t, struct{}{}, output)
|
||||
})
|
||||
|
||||
t.Run("can be chained with other IO operations", func(t *testing.T) {
|
||||
var captured int
|
||||
consumer := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
result := F.Pipe2(
|
||||
Of(21),
|
||||
ChainConsumer(consumer),
|
||||
Map(func(struct{}) int { return captured * 2 }),
|
||||
)
|
||||
|
||||
output := result()
|
||||
assert.Equal(t, 42, output)
|
||||
assert.Equal(t, 21, captured)
|
||||
})
|
||||
|
||||
t.Run("works with string values", func(t *testing.T) {
|
||||
var captured string
|
||||
consumer := func(s string) {
|
||||
captured = s
|
||||
}
|
||||
|
||||
result := F.Pipe1(
|
||||
Of("hello"),
|
||||
ChainConsumer(consumer),
|
||||
)
|
||||
|
||||
result()
|
||||
assert.Equal(t, "hello", captured)
|
||||
})
|
||||
|
||||
t.Run("works with complex types", func(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
var captured User
|
||||
consumer := func(u User) {
|
||||
captured = u
|
||||
}
|
||||
|
||||
user := User{Name: "Alice", Age: 30}
|
||||
result := F.Pipe1(
|
||||
Of(user),
|
||||
ChainConsumer(consumer),
|
||||
)
|
||||
|
||||
result()
|
||||
assert.Equal(t, user, captured)
|
||||
})
|
||||
|
||||
t.Run("multiple consumers in sequence", func(t *testing.T) {
|
||||
var values []int
|
||||
consumer1 := func(x int) {
|
||||
values = append(values, x)
|
||||
}
|
||||
consumer2 := func(_ struct{}) {
|
||||
values = append(values, 999)
|
||||
}
|
||||
|
||||
result := F.Pipe2(
|
||||
Of(42),
|
||||
ChainConsumer(consumer1),
|
||||
ChainConsumer(consumer2),
|
||||
)
|
||||
|
||||
result()
|
||||
assert.Equal(t, []int{42, 999}, values)
|
||||
})
|
||||
|
||||
t.Run("consumer with side effects", func(t *testing.T) {
|
||||
counter := 0
|
||||
consumer := func(x int) {
|
||||
counter += x
|
||||
}
|
||||
|
||||
// Execute multiple times
|
||||
op := ChainConsumer(consumer)
|
||||
io1 := op(Of(10))
|
||||
io2 := op(Of(20))
|
||||
io3 := op(Of(30))
|
||||
|
||||
io1()
|
||||
assert.Equal(t, 10, counter)
|
||||
|
||||
io2()
|
||||
assert.Equal(t, 30, counter)
|
||||
|
||||
io3()
|
||||
assert.Equal(t, 60, counter)
|
||||
})
|
||||
|
||||
t.Run("consumer in a pipeline with Map", func(t *testing.T) {
|
||||
var log []string
|
||||
logger := func(s string) {
|
||||
log = append(log, s)
|
||||
}
|
||||
|
||||
result := F.Pipe3(
|
||||
Of("start"),
|
||||
ChainConsumer(logger),
|
||||
Map(func(struct{}) string { return "middle" }),
|
||||
Chain(func(s string) IO[string] {
|
||||
logger(s)
|
||||
return Of("end")
|
||||
}),
|
||||
)
|
||||
|
||||
output := result()
|
||||
assert.Equal(t, "end", output)
|
||||
assert.Equal(t, []string{"start", "middle"}, log)
|
||||
})
|
||||
|
||||
t.Run("consumer does not affect IO chain on panic recovery", func(t *testing.T) {
|
||||
var captured int
|
||||
safeConsumer := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
result := F.Pipe2(
|
||||
Of(42),
|
||||
ChainConsumer(safeConsumer),
|
||||
Map(func(struct{}) string { return "success" }),
|
||||
)
|
||||
|
||||
output := result()
|
||||
assert.Equal(t, "success", output)
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
|
||||
t.Run("consumer with pointer types", func(t *testing.T) {
|
||||
var captured *int
|
||||
consumer := func(p *int) {
|
||||
captured = p
|
||||
}
|
||||
|
||||
value := 42
|
||||
result := F.Pipe1(
|
||||
Of(&value),
|
||||
ChainConsumer(consumer),
|
||||
)
|
||||
|
||||
result()
|
||||
assert.Equal(t, &value, captured)
|
||||
assert.Equal(t, 42, *captured)
|
||||
})
|
||||
|
||||
t.Run("consumer with slice accumulation", func(t *testing.T) {
|
||||
var accumulated []int
|
||||
consumer := func(x int) {
|
||||
accumulated = append(accumulated, x)
|
||||
}
|
||||
|
||||
op := ChainConsumer(consumer)
|
||||
|
||||
// Create multiple IOs and execute them
|
||||
for i := 1; i <= 5; i++ {
|
||||
io := op(Of(i))
|
||||
io()
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, accumulated)
|
||||
})
|
||||
|
||||
t.Run("consumer with map accumulation", func(t *testing.T) {
|
||||
counts := make(map[string]int)
|
||||
consumer := func(s string) {
|
||||
counts[s]++
|
||||
}
|
||||
|
||||
op := ChainConsumer(consumer)
|
||||
|
||||
words := []string{"hello", "world", "hello", "test", "world", "hello"}
|
||||
for _, word := range words {
|
||||
io := op(Of(word))
|
||||
io()
|
||||
}
|
||||
|
||||
assert.Equal(t, 3, counts["hello"])
|
||||
assert.Equal(t, 2, counts["world"])
|
||||
assert.Equal(t, 1, counts["test"])
|
||||
})
|
||||
|
||||
t.Run("lazy evaluation - consumer not called until IO executed", func(t *testing.T) {
|
||||
called := false
|
||||
consumer := func(x int) {
|
||||
called = true
|
||||
}
|
||||
|
||||
// Create the IO but don't execute it
|
||||
io := F.Pipe1(
|
||||
Of(42),
|
||||
ChainConsumer(consumer),
|
||||
)
|
||||
|
||||
// Consumer should not be called yet
|
||||
assert.False(t, called)
|
||||
|
||||
// Now execute
|
||||
io()
|
||||
|
||||
// Consumer should be called now
|
||||
assert.True(t, called)
|
||||
})
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -158,8 +159,9 @@ func TestWithTime(t *testing.T) {
|
||||
result := WithTime(Of(42))
|
||||
tuple := result()
|
||||
|
||||
assert.Equal(t, 42, tuple.F1)
|
||||
assert.True(t, tuple.F2.Before(tuple.F3) || tuple.F2.Equal(tuple.F3))
|
||||
assert.Equal(t, 42, pair.Tail(tuple))
|
||||
rg := pair.Head(tuple)
|
||||
assert.True(t, pair.Head(rg).Before(pair.Tail(rg)) || pair.Head(rg).Equal(pair.Tail(rg)))
|
||||
}
|
||||
|
||||
// Test WithDuration
|
||||
@@ -170,8 +172,8 @@ func TestWithDuration(t *testing.T) {
|
||||
})
|
||||
tuple := result()
|
||||
|
||||
assert.Equal(t, 42, tuple.F1)
|
||||
assert.True(t, tuple.F2 >= 10*time.Millisecond)
|
||||
assert.Equal(t, 42, pair.Tail(tuple))
|
||||
assert.True(t, pair.Head(tuple) >= 10*time.Millisecond)
|
||||
}
|
||||
|
||||
// Test Let
|
||||
|
||||
12
v2/io/doc.go
12
v2/io/doc.go
@@ -19,6 +19,18 @@
|
||||
// Unlike functions that execute immediately, IO values describe computations that will be
|
||||
// executed when explicitly invoked.
|
||||
//
|
||||
// # Fantasy Land Specification
|
||||
//
|
||||
// This implementation corresponds to the Fantasy Land IO type:
|
||||
// https://github.com/fantasyland/fantasy-land
|
||||
//
|
||||
// Implemented Fantasy Land algebras:
|
||||
// - Functor: https://github.com/fantasyland/fantasy-land#functor
|
||||
// - Apply: https://github.com/fantasyland/fantasy-land#apply
|
||||
// - Applicative: https://github.com/fantasyland/fantasy-land#applicative
|
||||
// - Chain: https://github.com/fantasyland/fantasy-land#chain
|
||||
// - Monad: https://github.com/fantasyland/fantasy-land#monad
|
||||
//
|
||||
// # Core Concepts
|
||||
//
|
||||
// The IO type is defined as a function that takes no arguments and returns a value:
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
C "github.com/IBM/fp-go/v2/internal/chain"
|
||||
FC "github.com/IBM/fp-go/v2/internal/functor"
|
||||
L "github.com/IBM/fp-go/v2/internal/lazy"
|
||||
P "github.com/IBM/fp-go/v2/pair"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
)
|
||||
|
||||
@@ -197,11 +198,11 @@ func WithTime[GTA ~func() T.Tuple3[A, time.Time, time.Time], GA ~func() A, A any
|
||||
}
|
||||
|
||||
// WithDuration returns an operation that measures the duration of the operation
|
||||
func WithDuration[GTA ~func() T.Tuple2[A, time.Duration], GA ~func() A, A any](a GA) GTA {
|
||||
return MakeIO[GTA](func() T.Tuple2[A, time.Duration] {
|
||||
func WithDuration[GTA ~func() P.Pair[time.Duration, A], GA ~func() A, A any](a GA) GTA {
|
||||
return MakeIO[GTA](func() P.Pair[time.Duration, A] {
|
||||
t0 := time.Now()
|
||||
res := a()
|
||||
t1 := time.Now()
|
||||
return T.MakeTuple2(res, t1.Sub(t0))
|
||||
return P.MakePair(t1.Sub(t0), res)
|
||||
})
|
||||
}
|
||||
|
||||
85
v2/io/io.go
85
v2/io/io.go
@@ -23,10 +23,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/internal/chain"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
INTL "github.com/IBM/fp-go/v2/internal/lazy"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
R "github.com/IBM/fp-go/v2/reader"
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -39,17 +36,6 @@ var (
|
||||
undefined = struct{}{}
|
||||
)
|
||||
|
||||
type (
|
||||
// IO represents a synchronous computation that cannot fail
|
||||
// refer to [https://andywhite.xyz/posts/2021-01-27-rte-foundations/#ioltagt] for more details
|
||||
IO[A any] = func() A
|
||||
|
||||
Kleisli[A, B any] = R.Reader[A, IO[B]]
|
||||
Operator[A, B any] = Kleisli[IO[A], B]
|
||||
Monoid[A any] = M.Monoid[IO[A]]
|
||||
Semigroup[A any] = S.Semigroup[IO[A]]
|
||||
)
|
||||
|
||||
// Of wraps a pure value in an IO context, creating a computation that returns that value.
|
||||
// This is the monadic return operation for IO.
|
||||
//
|
||||
@@ -57,12 +43,16 @@ type (
|
||||
//
|
||||
// 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
|
||||
}
|
||||
@@ -77,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)
|
||||
}
|
||||
@@ -86,9 +78,12 @@ func MonadOf[A any](a A) IO[A] {
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// doubled := io.MonadMap(io.Of(21), func(n int) int { return n * 2 })
|
||||
// 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())
|
||||
}
|
||||
@@ -99,8 +94,10 @@ func MonadMap[A, B any](fa IO[A], f func(A) B) IO[B] {
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// double := io.Map(func(n int) int { return n * 2 })
|
||||
// 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)
|
||||
}
|
||||
@@ -111,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))
|
||||
}
|
||||
@@ -153,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)
|
||||
@@ -167,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)
|
||||
}
|
||||
@@ -191,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)
|
||||
}
|
||||
@@ -299,7 +317,7 @@ func Defer[A any](gen func() IO[A]) IO[A] {
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// addFive := io.Of(func(n int) int { return n + 5 })
|
||||
// addFive := io.Of(N.Add(5))
|
||||
// result := io.MonadFlap(addFive, 10) // returns IO[15]
|
||||
func MonadFlap[B, A any](fab IO[func(A) B], a A) IO[B] {
|
||||
return functor.MonadFlap(MonadMap[func(A) B, B], fab, a)
|
||||
@@ -358,34 +376,45 @@ func After[A any](timestamp time.Time) Operator[A, A] {
|
||||
}
|
||||
|
||||
// WithTime returns an IO that measures the start and end time.Time of the operation.
|
||||
// Returns a tuple containing the result, start time, and end time.
|
||||
// Returns a Pair[Pair[time.Time, time.Time], A] where the head contains a nested pair of
|
||||
// (start time, end time) and the tail contains the result. The result is placed in the tail
|
||||
// position because that is the value that the pair monad operates on, allowing monadic
|
||||
// operations to transform the result while preserving the timing information.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// timed := io.WithTime(expensiveComputation)
|
||||
// result, start, end := timed()
|
||||
func WithTime[A any](a IO[A]) IO[T.Tuple3[A, time.Time, time.Time]] {
|
||||
return func() T.Tuple3[A, time.Time, time.Time] {
|
||||
// p := timed()
|
||||
// times := pair.Head(p) // Pair[time.Time, time.Time]
|
||||
// result := pair.Tail(p) // A
|
||||
// start := pair.Head(times) // time.Time
|
||||
// end := pair.Tail(times) // time.Time
|
||||
func WithTime[A any](a IO[A]) IO[Pair[Pair[time.Time, time.Time], A]] {
|
||||
return func() Pair[Pair[time.Time, time.Time], A] {
|
||||
t0 := time.Now()
|
||||
res := a()
|
||||
t1 := time.Now()
|
||||
return T.MakeTuple3(res, t0, t1)
|
||||
return pair.MakePair(pair.MakePair(t0, t1), res)
|
||||
}
|
||||
}
|
||||
|
||||
// WithDuration returns an IO that measures the execution time.Duration of the operation.
|
||||
// Returns a tuple containing the result and the duration.
|
||||
// Returns a Pair with the duration as the head and the result as the tail.
|
||||
// The result is placed in the tail position because that is the value that the pair monad
|
||||
// operates on, allowing monadic operations to transform the result while preserving the duration.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// timed := io.WithDuration(expensiveComputation)
|
||||
// result, duration := timed()
|
||||
// p := timed()
|
||||
// duration := pair.Head(p)
|
||||
// result := pair.Tail(p)
|
||||
// fmt.Printf("Took %v\n", duration)
|
||||
func WithDuration[A any](a IO[A]) IO[T.Tuple2[A, time.Duration]] {
|
||||
return func() T.Tuple2[A, time.Duration] {
|
||||
func WithDuration[A any](a IO[A]) IO[Pair[time.Duration, A]] {
|
||||
return func() Pair[time.Duration, A] {
|
||||
t0 := time.Now()
|
||||
res := a()
|
||||
t1 := time.Now()
|
||||
return T.MakeTuple2(res, t1.Sub(t0))
|
||||
return pair.MakePair(t1.Sub(t0), res)
|
||||
}
|
||||
}
|
||||
|
||||
147
v2/io/logging.go
147
v2/io/logging.go
@@ -18,7 +18,12 @@ package io
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/template"
|
||||
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
L "github.com/IBM/fp-go/v2/logging"
|
||||
)
|
||||
|
||||
@@ -32,13 +37,14 @@ import (
|
||||
// io.ChainFirst(io.Logger[User]()("Fetched user")),
|
||||
// processUser,
|
||||
// )
|
||||
func Logger[A any](loggers ...*log.Logger) func(string) Kleisli[A, any] {
|
||||
func Logger[A any](loggers ...*log.Logger) func(string) Kleisli[A, A] {
|
||||
_, right := L.LoggingCallbacks(loggers...)
|
||||
return func(prefix string) Kleisli[A, any] {
|
||||
return func(a A) IO[any] {
|
||||
return FromImpure(func() {
|
||||
return func(prefix string) Kleisli[A, A] {
|
||||
return func(a A) IO[A] {
|
||||
return func() A {
|
||||
right("%s: %v", prefix, a)
|
||||
})
|
||||
return a
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,11 +59,12 @@ func Logger[A any](loggers ...*log.Logger) func(string) Kleisli[A, any] {
|
||||
// io.ChainFirst(io.Logf[User]("User: %+v")),
|
||||
// processUser,
|
||||
// )
|
||||
func Logf[A any](prefix string) Kleisli[A, any] {
|
||||
return func(a A) IO[any] {
|
||||
return FromImpure(func() {
|
||||
func Logf[A any](prefix string) Kleisli[A, A] {
|
||||
return func(a A) IO[A] {
|
||||
return func() A {
|
||||
log.Printf(prefix, a)
|
||||
})
|
||||
return a
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,10 +79,124 @@ func Logf[A any](prefix string) Kleisli[A, any] {
|
||||
// io.ChainFirst(io.Printf[User]("User: %+v\n")),
|
||||
// processUser,
|
||||
// )
|
||||
func Printf[A any](prefix string) Kleisli[A, any] {
|
||||
return func(a A) IO[any] {
|
||||
return FromImpure(func() {
|
||||
func Printf[A any](prefix string) Kleisli[A, A] {
|
||||
return func(a A) IO[A] {
|
||||
return func() A {
|
||||
fmt.Printf(prefix, a)
|
||||
})
|
||||
return a
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleLogging is a helper function that creates a Kleisli arrow for logging/printing
|
||||
// values using Go template syntax. It lazily compiles the template on first use and
|
||||
// executes it with the provided value as data.
|
||||
//
|
||||
// Parameters:
|
||||
// - onSuccess: callback function to handle successfully formatted output
|
||||
// - onError: callback function to handle template parsing or execution errors
|
||||
// - prefix: Go template string to format the value
|
||||
//
|
||||
// The template is compiled lazily using sync.Once to ensure it's only parsed once.
|
||||
// The function always returns the original value unchanged, making it suitable for
|
||||
// use with ChainFirst or similar operations.
|
||||
func handleLoggingG(onSuccess func(string), onError func(error), prefix string) Kleisli[any, any] {
|
||||
var tmp *template.Template
|
||||
var err error
|
||||
var once sync.Once
|
||||
|
||||
init := func() {
|
||||
tmp, err = template.New("").Parse(prefix)
|
||||
}
|
||||
|
||||
return func(a any) IO[any] {
|
||||
return func() any {
|
||||
// make sure to compile lazily
|
||||
once.Do(init)
|
||||
if err == nil {
|
||||
var buffer strings.Builder
|
||||
tmpErr := tmp.Execute(&buffer, a)
|
||||
if tmpErr != nil {
|
||||
onError(tmpErr)
|
||||
onSuccess(fmt.Sprintf("%v", a))
|
||||
} else {
|
||||
onSuccess(buffer.String())
|
||||
}
|
||||
} else {
|
||||
onError(err)
|
||||
onSuccess(fmt.Sprintf("%v", a))
|
||||
}
|
||||
// in any case return the original value
|
||||
return a
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleLogging is a helper function that creates a Kleisli arrow for logging/printing
|
||||
// values using Go template syntax. It lazily compiles the template on first use and
|
||||
// executes it with the provided value as data.
|
||||
//
|
||||
// Parameters:
|
||||
// - onSuccess: callback function to handle successfully formatted output
|
||||
// - onError: callback function to handle template parsing or execution errors
|
||||
// - prefix: Go template string to format the value
|
||||
//
|
||||
// The template is compiled lazily using sync.Once to ensure it's only parsed once.
|
||||
// The function always returns the original value unchanged, making it suitable for
|
||||
// use with ChainFirst or similar operations.
|
||||
func handleLogging[A any](onSuccess func(string), onError func(error), prefix string) Kleisli[A, A] {
|
||||
generic := handleLoggingG(onSuccess, onError, prefix)
|
||||
return func(a A) IO[A] {
|
||||
return function.Pipe1(
|
||||
generic(a),
|
||||
MapTo[any](a),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// LogGo constructs a logger function using Go template syntax for formatting.
|
||||
// The prefix string is parsed as a Go template and executed with the value as data.
|
||||
// Both successful output and template errors are logged using log.Println.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
// result := pipe.Pipe2(
|
||||
// fetchUser(),
|
||||
// io.ChainFirst(io.LogGo[User]("User: {{.Name}}, Age: {{.Age}}")),
|
||||
// processUser,
|
||||
// )
|
||||
func LogGo[A any](prefix string) Kleisli[A, A] {
|
||||
return handleLogging[A](func(value string) {
|
||||
log.Println(value)
|
||||
}, func(err error) {
|
||||
log.Println(err)
|
||||
}, prefix)
|
||||
}
|
||||
|
||||
// PrintGo constructs a printer function using Go template syntax for formatting.
|
||||
// The prefix string is parsed as a Go template and executed with the value as data.
|
||||
// Successful output is printed to stdout using fmt.Println, while template errors
|
||||
// are printed to stderr using fmt.Fprintln.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
// result := pipe.Pipe2(
|
||||
// fetchUser(),
|
||||
// io.ChainFirst(io.PrintGo[User]("User: {{.Name}}, Age: {{.Age}}")),
|
||||
// processUser,
|
||||
// )
|
||||
func PrintGo[A any](prefix string) Kleisli[A, A] {
|
||||
return handleLogging[A](func(value string) {
|
||||
fmt.Println(value)
|
||||
}, func(err error) {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
}, prefix)
|
||||
}
|
||||
|
||||
@@ -16,25 +16,206 @@
|
||||
package io
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLogger(t *testing.T) {
|
||||
|
||||
l := Logger[int]()
|
||||
|
||||
lio := l("out")
|
||||
|
||||
assert.NotPanics(t, func() { lio(10)() })
|
||||
}
|
||||
|
||||
func TestLoggerWithCustomLogger(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
customLogger := log.New(&buf, "", 0)
|
||||
|
||||
l := Logger[int](customLogger)
|
||||
lio := l("test value")
|
||||
|
||||
result := lio(42)()
|
||||
|
||||
assert.Equal(t, 42, result)
|
||||
assert.Contains(t, buf.String(), "test value")
|
||||
assert.Contains(t, buf.String(), "42")
|
||||
}
|
||||
|
||||
func TestLoggerReturnsOriginalValue(t *testing.T) {
|
||||
type TestStruct struct {
|
||||
Name string
|
||||
Value int
|
||||
}
|
||||
|
||||
l := Logger[TestStruct]()
|
||||
lio := l("test")
|
||||
|
||||
input := TestStruct{Name: "test", Value: 100}
|
||||
result := lio(input)()
|
||||
|
||||
assert.Equal(t, input, result)
|
||||
}
|
||||
|
||||
func TestLogf(t *testing.T) {
|
||||
|
||||
l := Logf[int]
|
||||
|
||||
lio := l("Value is %d")
|
||||
|
||||
assert.NotPanics(t, func() { lio(10)() })
|
||||
}
|
||||
|
||||
func TestLogfReturnsOriginalValue(t *testing.T) {
|
||||
l := Logf[string]
|
||||
lio := l("String: %s")
|
||||
|
||||
input := "hello"
|
||||
result := lio(input)()
|
||||
|
||||
assert.Equal(t, input, result)
|
||||
}
|
||||
|
||||
func TestPrintfLogger(t *testing.T) {
|
||||
l := Printf[int]
|
||||
lio := l("Value: %d\n")
|
||||
assert.NotPanics(t, func() { lio(10)() })
|
||||
}
|
||||
|
||||
func TestPrintfLoggerReturnsOriginalValue(t *testing.T) {
|
||||
l := Printf[float64]
|
||||
lio := l("Number: %.2f\n")
|
||||
|
||||
input := 3.14159
|
||||
result := lio(input)()
|
||||
|
||||
assert.Equal(t, input, result)
|
||||
}
|
||||
|
||||
func TestLogGo(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
l := LogGo[User]
|
||||
lio := l("User: {{.Name}}, Age: {{.Age}}")
|
||||
|
||||
input := User{Name: "Alice", Age: 30}
|
||||
assert.NotPanics(t, func() { lio(input)() })
|
||||
}
|
||||
|
||||
func TestLogGoReturnsOriginalValue(t *testing.T) {
|
||||
type Product struct {
|
||||
ID int
|
||||
Name string
|
||||
Price float64
|
||||
}
|
||||
|
||||
l := LogGo[Product]
|
||||
lio := l("Product: {{.Name}} ({{.ID}})")
|
||||
|
||||
input := Product{ID: 123, Name: "Widget", Price: 19.99}
|
||||
result := lio(input)()
|
||||
|
||||
assert.Equal(t, input, result)
|
||||
}
|
||||
|
||||
func TestLogGoWithInvalidTemplate(t *testing.T) {
|
||||
l := LogGo[int]
|
||||
// Invalid template syntax
|
||||
lio := l("Value: {{.MissingField")
|
||||
|
||||
// Should not panic even with invalid template
|
||||
assert.NotPanics(t, func() { lio(42)() })
|
||||
}
|
||||
|
||||
func TestLogGoWithComplexTemplate(t *testing.T) {
|
||||
type Address struct {
|
||||
Street string
|
||||
City string
|
||||
}
|
||||
type Person struct {
|
||||
Name string
|
||||
Address Address
|
||||
}
|
||||
|
||||
l := LogGo[Person]
|
||||
lio := l("Person: {{.Name}} from {{.Address.City}}")
|
||||
|
||||
input := Person{
|
||||
Name: "Bob",
|
||||
Address: Address{Street: "Main St", City: "NYC"},
|
||||
}
|
||||
result := lio(input)()
|
||||
|
||||
assert.Equal(t, input, result)
|
||||
}
|
||||
|
||||
func TestPrintGo(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
l := PrintGo[User]
|
||||
lio := l("User: {{.Name}}, Age: {{.Age}}")
|
||||
|
||||
input := User{Name: "Charlie", Age: 25}
|
||||
assert.NotPanics(t, func() { lio(input)() })
|
||||
}
|
||||
|
||||
func TestPrintGoReturnsOriginalValue(t *testing.T) {
|
||||
type Score struct {
|
||||
Player string
|
||||
Points int
|
||||
}
|
||||
|
||||
l := PrintGo[Score]
|
||||
lio := l("{{.Player}}: {{.Points}} points")
|
||||
|
||||
input := Score{Player: "Alice", Points: 100}
|
||||
result := lio(input)()
|
||||
|
||||
assert.Equal(t, input, result)
|
||||
}
|
||||
|
||||
func TestPrintGoWithInvalidTemplate(t *testing.T) {
|
||||
l := PrintGo[string]
|
||||
// Invalid template syntax
|
||||
lio := l("Value: {{.}")
|
||||
|
||||
// Should not panic even with invalid template
|
||||
assert.NotPanics(t, func() { lio("test")() })
|
||||
}
|
||||
|
||||
func TestLogGoInPipeline(t *testing.T) {
|
||||
type Data struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
input := Data{Value: 10}
|
||||
|
||||
result := F.Pipe2(
|
||||
Of(input),
|
||||
ChainFirst(LogGo[Data]("Processing: {{.Value}}")),
|
||||
Map(func(d Data) Data {
|
||||
return Data{Value: d.Value * 2}
|
||||
}),
|
||||
)()
|
||||
|
||||
assert.Equal(t, 20, result.Value)
|
||||
}
|
||||
|
||||
func TestPrintGoInPipeline(t *testing.T) {
|
||||
input := "hello"
|
||||
|
||||
result := F.Pipe2(
|
||||
Of(input),
|
||||
ChainFirst(PrintGo[string]("Input: {{.}}")),
|
||||
Map(func(s string) string {
|
||||
return s + " world"
|
||||
}),
|
||||
)()
|
||||
|
||||
assert.Equal(t, "hello world", result)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
package io
|
||||
|
||||
import "iter"
|
||||
import (
|
||||
"iter"
|
||||
|
||||
"github.com/IBM/fp-go/v2/consumer"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
// IO represents a synchronous computation that cannot fail
|
||||
// refer to [https://andywhite.xyz/posts/2021-01-27-rte-foundations/#ioltagt] for more details
|
||||
IO[A any] = func() A
|
||||
Pair[L, R any] = pair.Pair[L, R]
|
||||
|
||||
Kleisli[A, B any] = reader.Reader[A, IO[B]]
|
||||
Operator[A, B any] = Kleisli[IO[A], B]
|
||||
Monoid[A any] = M.Monoid[IO[A]]
|
||||
Semigroup[A any] = S.Semigroup[IO[A]]
|
||||
|
||||
Consumer[A any] = consumer.Consumer[A]
|
||||
|
||||
Seq[T any] = iter.Seq[T]
|
||||
)
|
||||
|
||||
90
v2/ioeither/consumer.go
Normal file
90
v2/ioeither/consumer.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ioeither
|
||||
|
||||
import "github.com/IBM/fp-go/v2/io"
|
||||
|
||||
// ChainConsumer converts a Consumer into an IOEither operator that executes the consumer
|
||||
// as a side effect on successful (Right) values and returns an empty struct.
|
||||
//
|
||||
// This function bridges the gap between pure consumers (functions that consume values
|
||||
// without returning anything) and the IOEither monad. It takes a Consumer[A] and returns
|
||||
// an Operator that:
|
||||
// 1. If the IOEither is Right, executes the consumer with the value as a side effect
|
||||
// 2. If the IOEither is Left, propagates the error without calling the consumer
|
||||
// 3. Returns IOEither[E, struct{}] to maintain the monadic chain
|
||||
//
|
||||
// The consumer is only executed for successful (Right) values. Errors (Left values) are
|
||||
// propagated unchanged. This is useful for operations like logging successful results,
|
||||
// collecting metrics, or updating external state within an IOEither pipeline.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The error type of the IOEither
|
||||
// - A: The type of value consumed by the consumer
|
||||
//
|
||||
// Parameters:
|
||||
// - c: A Consumer[A] that performs side effects on values of type A
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator[E, A, struct{}] that executes the consumer on Right values and returns an empty struct
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a consumer that logs successful values
|
||||
// logger := func(x int) {
|
||||
// fmt.Printf("Success: %d\n", x)
|
||||
// }
|
||||
//
|
||||
// // Convert it to an IOEither operator
|
||||
// logOp := ioeither.ChainConsumer[error](logger)
|
||||
//
|
||||
// // Use it in an IOEither pipeline
|
||||
// result := F.Pipe2(
|
||||
// ioeither.Right[error](42),
|
||||
// logOp, // Logs "Success: 42"
|
||||
// ioeither.Map[error](func(struct{}) string { return "done" }),
|
||||
// )
|
||||
// result() // Returns Right("done") after logging
|
||||
//
|
||||
// // Errors are propagated without calling the consumer
|
||||
// errorResult := F.Pipe2(
|
||||
// ioeither.Left[int](errors.New("failed")),
|
||||
// logOp, // Consumer NOT called
|
||||
// ioeither.Map[error](func(struct{}) string { return "done" }),
|
||||
// )
|
||||
// errorResult() // Returns Left(error) without logging
|
||||
//
|
||||
// // Example with data collection
|
||||
// var successfulValues []int
|
||||
// collector := func(x int) {
|
||||
// successfulValues = append(successfulValues, x)
|
||||
// }
|
||||
//
|
||||
// pipeline := F.Pipe2(
|
||||
// ioeither.Right[error](100),
|
||||
// ioeither.ChainConsumer[error](collector), // Collects the value
|
||||
// ioeither.Map[error](func(struct{}) int { return len(successfulValues) }),
|
||||
// )
|
||||
// count := pipeline() // Returns Right(1), successfulValues contains [100]
|
||||
//go:inline
|
||||
func ChainConsumer[E, A any](c Consumer[A]) Operator[E, A, struct{}] {
|
||||
return ChainIOK[E](io.FromConsumerK(c))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainFirstConsumer[E, A any](c Consumer[A]) Operator[E, A, A] {
|
||||
return ChainFirstIOK[E](io.FromConsumerK(c))
|
||||
}
|
||||
378
v2/ioeither/consumer_test.go
Normal file
378
v2/ioeither/consumer_test.go
Normal file
@@ -0,0 +1,378 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ioeither
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestChainConsumer(t *testing.T) {
|
||||
t.Run("executes consumer with Right value", func(t *testing.T) {
|
||||
var captured int
|
||||
consumer := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
result := F.Pipe1(
|
||||
Right[error](42),
|
||||
ChainConsumer[error](consumer),
|
||||
)
|
||||
|
||||
// Execute the IOEither
|
||||
result()
|
||||
|
||||
// Verify the consumer was called with the correct value
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
|
||||
t.Run("does not execute consumer with Left value", func(t *testing.T) {
|
||||
called := false
|
||||
consumer := func(x int) {
|
||||
called = true
|
||||
}
|
||||
|
||||
result := F.Pipe1(
|
||||
Left[int](errors.New("error")),
|
||||
ChainConsumer[error](consumer),
|
||||
)
|
||||
|
||||
// Execute the IOEither
|
||||
result()
|
||||
|
||||
// Verify the consumer was NOT called
|
||||
assert.False(t, called)
|
||||
})
|
||||
|
||||
t.Run("returns Right with empty struct for Right input", func(t *testing.T) {
|
||||
consumer := func(x int) {
|
||||
// no-op consumer
|
||||
}
|
||||
|
||||
result := F.Pipe1(
|
||||
Right[error](100),
|
||||
ChainConsumer[error](consumer),
|
||||
)
|
||||
|
||||
// Execute and verify return type
|
||||
output := result()
|
||||
assert.Equal(t, E.Of[error](struct{}{}), output)
|
||||
})
|
||||
|
||||
t.Run("returns Left unchanged for Left input", func(t *testing.T) {
|
||||
consumer := func(x int) {
|
||||
// no-op consumer
|
||||
}
|
||||
|
||||
err := errors.New("test error")
|
||||
result := F.Pipe1(
|
||||
Left[int](err),
|
||||
ChainConsumer[error](consumer),
|
||||
)
|
||||
|
||||
// Execute and verify error is preserved
|
||||
output := result()
|
||||
assert.True(t, E.IsLeft(output))
|
||||
_, leftErr := E.Unwrap(output)
|
||||
assert.Equal(t, err, leftErr)
|
||||
})
|
||||
|
||||
t.Run("can be chained with other IOEither operations", func(t *testing.T) {
|
||||
var captured int
|
||||
consumer := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
result := F.Pipe2(
|
||||
Right[error](21),
|
||||
ChainConsumer[error](consumer),
|
||||
Map[error](func(struct{}) int { return captured * 2 }),
|
||||
)
|
||||
|
||||
output := result()
|
||||
assert.Equal(t, E.Right[error](42), output)
|
||||
assert.Equal(t, 21, captured)
|
||||
})
|
||||
|
||||
t.Run("works with string values", func(t *testing.T) {
|
||||
var captured string
|
||||
consumer := func(s string) {
|
||||
captured = s
|
||||
}
|
||||
|
||||
result := F.Pipe1(
|
||||
Right[error]("hello"),
|
||||
ChainConsumer[error](consumer),
|
||||
)
|
||||
|
||||
result()
|
||||
assert.Equal(t, "hello", captured)
|
||||
})
|
||||
|
||||
t.Run("works with complex types", func(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
var captured User
|
||||
consumer := func(u User) {
|
||||
captured = u
|
||||
}
|
||||
|
||||
user := User{Name: "Alice", Age: 30}
|
||||
result := F.Pipe1(
|
||||
Right[error](user),
|
||||
ChainConsumer[error](consumer),
|
||||
)
|
||||
|
||||
result()
|
||||
assert.Equal(t, user, captured)
|
||||
})
|
||||
|
||||
t.Run("multiple consumers in sequence", func(t *testing.T) {
|
||||
var values []int
|
||||
consumer1 := func(x int) {
|
||||
values = append(values, x)
|
||||
}
|
||||
consumer2 := func(_ struct{}) {
|
||||
values = append(values, 999)
|
||||
}
|
||||
|
||||
result := F.Pipe2(
|
||||
Right[error](42),
|
||||
ChainConsumer[error](consumer1),
|
||||
ChainConsumer[error](consumer2),
|
||||
)
|
||||
|
||||
result()
|
||||
assert.Equal(t, []int{42, 999}, values)
|
||||
})
|
||||
|
||||
t.Run("consumer with side effects on Right values only", func(t *testing.T) {
|
||||
counter := 0
|
||||
consumer := func(x int) {
|
||||
counter += x
|
||||
}
|
||||
|
||||
op := ChainConsumer[error](consumer)
|
||||
|
||||
// Execute with Right values
|
||||
io1 := op(Right[error](10))
|
||||
io2 := op(Right[error](20))
|
||||
io3 := op(Left[int](errors.New("error")))
|
||||
io4 := op(Right[error](30))
|
||||
|
||||
io1()
|
||||
assert.Equal(t, 10, counter)
|
||||
|
||||
io2()
|
||||
assert.Equal(t, 30, counter)
|
||||
|
||||
io3() // Should not increment counter
|
||||
assert.Equal(t, 30, counter)
|
||||
|
||||
io4()
|
||||
assert.Equal(t, 60, counter)
|
||||
})
|
||||
|
||||
t.Run("consumer in a pipeline with Map and Chain", func(t *testing.T) {
|
||||
var log []string
|
||||
logger := func(s string) {
|
||||
log = append(log, s)
|
||||
}
|
||||
|
||||
result := F.Pipe3(
|
||||
Right[error]("start"),
|
||||
ChainConsumer[error](logger),
|
||||
Map[error](func(struct{}) string { return "middle" }),
|
||||
Chain(func(s string) IOEither[error, string] {
|
||||
logger(s)
|
||||
return Right[error]("end")
|
||||
}),
|
||||
)
|
||||
|
||||
output := result()
|
||||
assert.Equal(t, E.Right[error]("end"), output)
|
||||
assert.Equal(t, []string{"start", "middle"}, log)
|
||||
})
|
||||
|
||||
t.Run("error propagation through consumer chain", func(t *testing.T) {
|
||||
var captured []int
|
||||
consumer := func(x int) {
|
||||
captured = append(captured, x)
|
||||
}
|
||||
|
||||
err := errors.New("early error")
|
||||
result := F.Pipe3(
|
||||
Left[int](err),
|
||||
ChainConsumer[error](consumer),
|
||||
Map[error](func(struct{}) int { return 100 }),
|
||||
ChainConsumer[error](consumer),
|
||||
)
|
||||
|
||||
output := result()
|
||||
assert.True(t, E.IsLeft(output))
|
||||
_, leftErr := E.Unwrap(output)
|
||||
assert.Equal(t, err, leftErr)
|
||||
assert.Empty(t, captured) // Consumer never called
|
||||
})
|
||||
|
||||
t.Run("consumer with pointer types", func(t *testing.T) {
|
||||
var captured *int
|
||||
consumer := func(p *int) {
|
||||
captured = p
|
||||
}
|
||||
|
||||
value := 42
|
||||
result := F.Pipe1(
|
||||
Right[error](&value),
|
||||
ChainConsumer[error](consumer),
|
||||
)
|
||||
|
||||
result()
|
||||
assert.Equal(t, &value, captured)
|
||||
assert.Equal(t, 42, *captured)
|
||||
})
|
||||
|
||||
t.Run("consumer with slice accumulation", func(t *testing.T) {
|
||||
var accumulated []int
|
||||
consumer := func(x int) {
|
||||
accumulated = append(accumulated, x)
|
||||
}
|
||||
|
||||
op := ChainConsumer[error](consumer)
|
||||
|
||||
// Create multiple IOEithers and execute them
|
||||
for i := 1; i <= 5; i++ {
|
||||
io := op(Right[error](i))
|
||||
io()
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, accumulated)
|
||||
})
|
||||
|
||||
t.Run("consumer with map accumulation", func(t *testing.T) {
|
||||
counts := make(map[string]int)
|
||||
consumer := func(s string) {
|
||||
counts[s]++
|
||||
}
|
||||
|
||||
op := ChainConsumer[error](consumer)
|
||||
|
||||
words := []string{"hello", "world", "hello", "test", "world", "hello"}
|
||||
for _, word := range words {
|
||||
io := op(Right[error](word))
|
||||
io()
|
||||
}
|
||||
|
||||
assert.Equal(t, 3, counts["hello"])
|
||||
assert.Equal(t, 2, counts["world"])
|
||||
assert.Equal(t, 1, counts["test"])
|
||||
})
|
||||
|
||||
t.Run("lazy evaluation - consumer not called until IOEither executed", func(t *testing.T) {
|
||||
called := false
|
||||
consumer := func(x int) {
|
||||
called = true
|
||||
}
|
||||
|
||||
// Create the IOEither but don't execute it
|
||||
io := F.Pipe1(
|
||||
Right[error](42),
|
||||
ChainConsumer[error](consumer),
|
||||
)
|
||||
|
||||
// Consumer should not be called yet
|
||||
assert.False(t, called)
|
||||
|
||||
// Now execute
|
||||
io()
|
||||
|
||||
// Consumer should be called now
|
||||
assert.True(t, called)
|
||||
})
|
||||
|
||||
t.Run("consumer with different error types", func(t *testing.T) {
|
||||
var captured int
|
||||
consumer := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
// Test with string error type
|
||||
result1 := F.Pipe1(
|
||||
Right[string](100),
|
||||
ChainConsumer[string](consumer),
|
||||
)
|
||||
result1()
|
||||
assert.Equal(t, 100, captured)
|
||||
|
||||
// Test with custom error type
|
||||
type CustomError struct {
|
||||
Code int
|
||||
Message string
|
||||
}
|
||||
result2 := F.Pipe1(
|
||||
Right[CustomError](200),
|
||||
ChainConsumer[CustomError](consumer),
|
||||
)
|
||||
result2()
|
||||
assert.Equal(t, 200, captured)
|
||||
})
|
||||
|
||||
t.Run("consumer in error recovery scenario", func(t *testing.T) {
|
||||
var successLog []int
|
||||
successConsumer := func(x int) {
|
||||
successLog = append(successLog, x)
|
||||
}
|
||||
|
||||
result := F.Pipe2(
|
||||
Left[int](errors.New("initial error")),
|
||||
ChainLeft(func(e error) IOEither[error, int] {
|
||||
// Recover from error
|
||||
return Right[error](42)
|
||||
}),
|
||||
ChainConsumer[error](successConsumer),
|
||||
)
|
||||
|
||||
output := result()
|
||||
assert.True(t, E.IsRight(output))
|
||||
assert.Equal(t, []int{42}, successLog)
|
||||
})
|
||||
|
||||
t.Run("consumer composition with ChainFirst", func(t *testing.T) {
|
||||
var log []string
|
||||
logger := func(s string) {
|
||||
log = append(log, "Logged: "+s)
|
||||
}
|
||||
|
||||
result := F.Pipe2(
|
||||
Right[error]("test"),
|
||||
ChainConsumer[error](logger),
|
||||
ChainFirst(func(_ struct{}) IOEither[error, int] {
|
||||
return Right[error](42)
|
||||
}),
|
||||
)
|
||||
|
||||
output := result()
|
||||
assert.True(t, E.IsRight(output))
|
||||
assert.Equal(t, []string{"Logged: test"}, log)
|
||||
})
|
||||
}
|
||||
@@ -13,6 +13,28 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package ioeither provides the IOEither monad, combining IO effects with Either for error handling.
|
||||
//
|
||||
// # Fantasy Land Specification
|
||||
//
|
||||
// This is a monad transformer combining:
|
||||
// - IO monad: https://github.com/fantasyland/fantasy-land
|
||||
// - Either monad: https://github.com/fantasyland/fantasy-land#either
|
||||
//
|
||||
// Implemented Fantasy Land algebras:
|
||||
// - Functor: https://github.com/fantasyland/fantasy-land#functor
|
||||
// - Bifunctor: https://github.com/fantasyland/fantasy-land#bifunctor
|
||||
// - Apply: https://github.com/fantasyland/fantasy-land#apply
|
||||
// - Applicative: https://github.com/fantasyland/fantasy-land#applicative
|
||||
// - Chain: https://github.com/fantasyland/fantasy-land#chain
|
||||
// - Monad: https://github.com/fantasyland/fantasy-land#monad
|
||||
// - Alt: https://github.com/fantasyland/fantasy-land#alt
|
||||
//
|
||||
// IOEither[E, A] represents a computation that:
|
||||
// - Performs side effects (IO)
|
||||
// - Can fail with an error of type E or succeed with a value of type A (Either)
|
||||
//
|
||||
// This is defined as: IO[Either[E, A]] or func() Either[E, A]
|
||||
package ioeither
|
||||
|
||||
//go:generate go run .. ioeither --count 10 --filename gen.go
|
||||
|
||||
@@ -879,7 +879,7 @@ func Eitherize7[F ~func(T1, T2, T3, T4, T5, T6, T7) (R, error), T1, T2, T3, T4,
|
||||
|
||||
// Uneitherize7 converts a function with 8 parameters returning a tuple into a function with 7 parameters returning a [IOEither[error, R]]
|
||||
func Uneitherize7[F ~func(T1, T2, T3, T4, T5, T6, T7) IOEither[error, R], T1, T2, T3, T4, T5, T6, T7, R any](f F) func(T1, T2, T3, T4, T5, T6, T7) (R, error) {
|
||||
return G.Uneitherize7[IOEither[error, R]](f)
|
||||
return G.Uneitherize7(f)
|
||||
}
|
||||
|
||||
// SequenceT7 converts 7 [IOEither[E, T]] into a [IOEither[E, tuple.Tuple7[T1, T2, T3, T4, T5, T6, T7]]]
|
||||
|
||||
@@ -279,6 +279,16 @@ func ChainTo[A, E, B any](fb IOEither[E, B]) Operator[E, A, B] {
|
||||
return Chain(function.Constant1[A](fb))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadChainToIO[E, A, B any](fa IOEither[E, A], fb IO[B]) IOEither[E, B] {
|
||||
return MonadChainTo(fa, FromIO[E](fb))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainToIO[E, A, B any](fb IO[B]) Operator[E, A, B] {
|
||||
return ChainTo[A](FromIO[E](fb))
|
||||
}
|
||||
|
||||
// MonadChainFirst runs the [IOEither] monad returned by the function but returns the result of the original monad
|
||||
func MonadChainFirst[E, A, B any](ma IOEither[E, A], f Kleisli[E, A, B]) IOEither[E, A] {
|
||||
return chain.MonadChainFirst(
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user