1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-12-07 23:03:15 +02:00

Compare commits

...

5 Commits

Author SHA1 Message Date
Dr. Carsten Leue
34826d8c52 fix: Ask and add tests to retry
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-04 16:47:53 +01:00
Dr. Carsten Leue
24c0519cc7 fix: try to unify type signatures
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-04 16:31:21 +01:00
Dr. Carsten Leue
ff48d8953e fix: implement some missing methods in reader io
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-04 13:50:25 +01:00
Dr. Carsten Leue
d739c9b277 fix: add doc to readerio
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-03 18:13:59 +01:00
Dr. Carsten Leue
f0054431a5 fix: add logging to readerio 2025-12-03 18:07:06 +01:00
52 changed files with 6842 additions and 409 deletions

574
v2/DESIGN.md Normal file
View 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

View File

@@ -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
View 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")
}

View File

@@ -0,0 +1,560 @@
// 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/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)
}

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

View File

@@ -0,0 +1,69 @@
// 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/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]
)

View File

@@ -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]),

View File

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

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

View File

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

View File

@@ -107,8 +107,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 +177,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 +211,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) }),
)
}

View File

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

View File

@@ -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

View File

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

View File

@@ -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.
//
@@ -86,7 +72,7 @@ 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
func MonadMap[A, B any](fa IO[A], f func(A) B) IO[B] {
return func() B {
@@ -99,7 +85,7 @@ 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))
func Map[A, B any](f func(A) B) Operator[A, B] {
return F.Bind2nd(MonadMap[A, B], f)
@@ -299,7 +285,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 +344,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)
}
}

View File

@@ -23,6 +23,7 @@ import (
"sync"
"text/template"
"github.com/IBM/fp-go/v2/function"
L "github.com/IBM/fp-go/v2/logging"
)
@@ -99,7 +100,7 @@ func Printf[A any](prefix string) Kleisli[A, A] {
// 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] {
func handleLoggingG(onSuccess func(string), onError func(error), prefix string) Kleisli[any, any] {
var tmp *template.Template
var err error
var once sync.Once
@@ -108,8 +109,8 @@ func handleLogging[A any](onSuccess func(string), onError func(error), prefix st
tmp, err = template.New("").Parse(prefix)
}
return func(a A) IO[A] {
return func() A {
return func(a any) IO[any] {
return func() any {
// make sure to compile lazily
once.Do(init)
if err == nil {
@@ -131,6 +132,28 @@ func handleLogging[A any](onSuccess func(string), onError func(error), prefix st
}
}
// 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.

View File

@@ -1,7 +1,25 @@
package io
import "iter"
import (
"iter"
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]]
Seq[T any] = iter.Seq[T]
)

View File

@@ -27,18 +27,51 @@ import (
)
type (
Option[A any] = option.Option[A]
Lazy[A any] = lazy.Lazy[A]
Pair[L, R any] = pair.Pair[L, R]
Predicate[A any] = predicate.Predicate[A]
IO[A any] = io.IO[A]
// Option represents an optional value that may or may not be present.
// It's an alias for option.Option[A] and is used to handle nullable values safely.
Option[A any] = option.Option[A]
// Iterator represents a stateless, pure way to iterate over a sequence
// Lazy represents a lazily evaluated computation that produces a value of type A.
// It's an alias for lazy.Lazy[A] and defers computation until the value is needed.
Lazy[A any] = lazy.Lazy[A]
// Pair represents a tuple of two values of types L and R.
// It's an alias for pair.Pair[L, R] where L is the head (left) and R is the tail (right).
Pair[L, R any] = pair.Pair[L, R]
// Predicate represents a function that tests a value of type A and returns a boolean.
// It's an alias for predicate.Predicate[A] and is used for filtering and testing operations.
Predicate[A any] = predicate.Predicate[A]
// IO represents a lazy computation that performs side effects and produces a value of type A.
// It's an alias for io.IO[A] and encapsulates effectful operations.
IO[A any] = io.IO[A]
// Iterator represents a stateless, pure, functional iterator over a sequence of values.
// It's defined as a lazy computation that returns an optional pair of (next iterator, current value).
// The stateless nature means each iteration step produces a new iterator, making it immutable
// and safe for concurrent use. When the sequence is exhausted, it returns None.
// The value is placed in the tail position of the pair because that is what the pair monad
// operates on, allowing monadic operations to transform values while preserving the iterator state.
Iterator[U any] Lazy[Option[Pair[Iterator[U], U]]]
Kleisli[A, B any] = reader.Reader[A, Iterator[B]]
// Kleisli represents a Kleisli arrow for the Iterator monad.
// It's a function from A to Iterator[B], which allows composition of
// monadic functions that produce iterators. This is the fundamental building
// block for chaining iterator operations.
Kleisli[A, B any] = reader.Reader[A, Iterator[B]]
// Operator is a specialized Kleisli arrow that operates on Iterator values.
// It transforms an Iterator[A] into an Iterator[B], making it useful for
// building pipelines of iterator transformations such as map, filter, and flatMap.
Operator[A, B any] = Kleisli[Iterator[A], B]
Seq[T any] = iter.Seq[T]
// Seq represents Go's standard library iterator type for single values.
// It's an alias for iter.Seq[T] and provides interoperability with Go 1.23+ range-over-func.
Seq[T any] = iter.Seq[T]
// Seq2 represents Go's standard library iterator type for key-value pairs.
// It's an alias for iter.Seq2[K, V] and provides interoperability with Go 1.23+ range-over-func
// for iterating over maps and other key-value structures.
Seq2[K, V any] = iter.Seq2[K, V]
)

View File

@@ -124,7 +124,7 @@ numbers := []int{1, 2, 3, 4, 5}
doubled := F.Pipe2(
numbers,
TA.Traversal[int](),
traversal.Modify[[]int, int](func(n int) int { return n * 2 }),
traversal.Modify[[]int, int](N.Mul(2)),
)
// Result: [2, 4, 6, 8, 10]
```

View File

@@ -27,14 +27,28 @@ import (
// setCopy wraps a setter for a pointer into a setter that first creates a copy before
// modifying that copy
func setCopy[SET ~func(*S, A) *S, S, A any](setter SET) func(s *S, a A) *S {
return func(s *S, a A) *S {
var empty S
safeSet := func(s *S, a A) *S {
// make sure we have a total implementation
cpy := *s
return setter(&cpy, a)
}
return func(s *S, a A) *S {
// make sure we have a total implementation
if s != nil {
return safeSet(s, a)
}
// fallback to the empty object
return safeSet(&empty, a)
}
}
func setCopyWithEq[GET ~func(*S) A, SET ~func(*S, A) *S, S, A any](pred EQ.Eq[A], getter GET, setter SET) func(s *S, a A) *S {
return func(s *S, a A) *S {
var empty S
safeSet := func(s *S, a A) *S {
if pred.Equals(getter(s), a) {
return s
}
@@ -42,17 +56,39 @@ func setCopyWithEq[GET ~func(*S) A, SET ~func(*S, A) *S, S, A any](pred EQ.Eq[A]
cpy := *s
return setter(&cpy, a)
}
return func(s *S, a A) *S {
// make sure we have a total implementation
if s != nil {
return safeSet(s, a)
}
// fallback to the empty object
return safeSet(&empty, a)
}
}
// setCopyCurried wraps a setter for a pointer into a setter that first creates a copy before
// modifying that copy
func setCopyCurried[SET ~func(A) Endomorphism[*S], S, A any](setter SET) func(A) Endomorphism[*S] {
var empty S
return func(a A) Endomorphism[*S] {
seta := setter(a)
return func(s *S) *S {
safeSet := func(s *S) *S {
// make sure we have a total implementation
cpy := *s
return seta(&cpy)
}
return func(s *S) *S {
// make sure we have a total implementation
if s != nil {
return safeSet(s)
}
// fallback to the empty object
return safeSet(&empty)
}
}
}
@@ -442,6 +478,8 @@ func compose[GET ~func(S) B, SET ~func(B) func(S) S, S, A, B any](creator func(g
// person := Person{Name: "Alice", Address: Address{Street: "Main St"}}
// street := personStreetLens.Get(person) // "Main St"
// updated := personStreetLens.Set("Oak Ave")(person)
//
//go:inline
func Compose[S, A, B any](ab Lens[A, B]) Operator[S, A, B] {
return compose(MakeLensCurried[func(S) B, func(B) func(S) S], ab)
}

View File

@@ -544,3 +544,396 @@ func TestMakeLensStrict_BoolField(t *testing.T) {
same := enabledLens.Set(true)(config)
assert.Same(t, config, same)
}
func TestMakeLensRef_WithNilState(t *testing.T) {
// Test that MakeLensRef creates a total lens that works with nil pointers
nameLens := MakeLensRef(
func(s *Street) string {
if s == nil {
return ""
}
return s.name
},
func(s *Street, name string) *Street {
s.name = name
return s
},
)
// Test Get with nil - should handle gracefully
var nilStreet *Street = nil
name := nameLens.Get(nilStreet)
assert.Equal(t, "", name)
// Test Set with nil - should create a new object with zero values except the set field
updated := nameLens.Set("NewStreet")(nilStreet)
assert.NotNil(t, updated)
assert.Equal(t, "NewStreet", updated.name)
assert.Equal(t, 0, updated.num) // Zero value for int
// Verify original nil pointer is unchanged
assert.Nil(t, nilStreet)
}
func TestMakeLensRef_WithNilState_IntField(t *testing.T) {
// Test with an int field lens
numLens := MakeLensRef(
func(s *Street) int {
if s == nil {
return 0
}
return s.num
},
func(s *Street, num int) *Street {
s.num = num
return s
},
)
var nilStreet *Street = nil
// Get from nil should return zero value
num := numLens.Get(nilStreet)
assert.Equal(t, 0, num)
// Set on nil should create new object
updated := numLens.Set(42)(nilStreet)
assert.NotNil(t, updated)
assert.Equal(t, 42, updated.num)
assert.Equal(t, "", updated.name) // Zero value for string
}
func TestMakeLensRef_WithNilState_Composed(t *testing.T) {
// Test composed lenses with nil state
streetLens := MakeLensRef(
func(s *Street) string {
if s == nil {
return ""
}
return s.name
},
(*Street).SetName,
)
addrLens := MakeLensRef(
func(a *Address) *Street {
if a == nil {
return nil
}
return a.street
},
(*Address).SetStreet,
)
// Compose the lenses
streetName := ComposeRef[Address](streetLens)(addrLens)
var nilAddress *Address = nil
// Get from nil should handle gracefully
name := streetName.Get(nilAddress)
assert.Equal(t, "", name)
// Set on nil should create new nested structure
updated := streetName.Set("TestStreet")(nilAddress)
assert.NotNil(t, updated)
assert.NotNil(t, updated.street)
assert.Equal(t, "TestStreet", updated.street.name)
assert.Equal(t, "", updated.city) // Zero value for city
}
func TestMakeLensRefCurried_WithNilState(t *testing.T) {
// Test that MakeLensRefCurried creates a total lens that works with nil pointers
nameLens := MakeLensRefCurried(
func(s *Street) string {
if s == nil {
return ""
}
return s.name
},
func(name string) func(*Street) *Street {
return func(s *Street) *Street {
s.name = name
return s
}
},
)
// Test Get with nil
var nilStreet *Street = nil
name := nameLens.Get(nilStreet)
assert.Equal(t, "", name)
// Test Set with nil - should create a new object
updated := nameLens.Set("CurriedStreet")(nilStreet)
assert.NotNil(t, updated)
assert.Equal(t, "CurriedStreet", updated.name)
assert.Equal(t, 0, updated.num) // Zero value for int
// Verify original nil pointer is unchanged
assert.Nil(t, nilStreet)
}
func TestMakeLensRefCurried_WithNilState_IntField(t *testing.T) {
// Test with an int field lens using curried setter
numLens := MakeLensRefCurried(
func(s *Street) int {
if s == nil {
return 0
}
return s.num
},
func(num int) func(*Street) *Street {
return func(s *Street) *Street {
s.num = num
return s
}
},
)
var nilStreet *Street = nil
// Get from nil should return zero value
num := numLens.Get(nilStreet)
assert.Equal(t, 0, num)
// Set on nil should create new object
updated := numLens.Set(99)(nilStreet)
assert.NotNil(t, updated)
assert.Equal(t, 99, updated.num)
assert.Equal(t, "", updated.name) // Zero value for string
}
func TestMakeLensRefCurried_WithNilState_MultipleOperations(t *testing.T) {
// Test multiple operations on nil and non-nil states
nameLens := MakeLensRefCurried(
func(s *Street) string {
if s == nil {
return ""
}
return s.name
},
func(name string) func(*Street) *Street {
return func(s *Street) *Street {
s.name = name
return s
}
},
)
var nilStreet *Street = nil
// First operation on nil
street1 := nameLens.Set("First")(nilStreet)
assert.NotNil(t, street1)
assert.Equal(t, "First", street1.name)
// Second operation on non-nil result
street2 := nameLens.Set("Second")(street1)
assert.NotNil(t, street2)
assert.Equal(t, "Second", street2.name)
assert.Equal(t, "First", street1.name) // Original unchanged
// Third operation back to nil (edge case)
street3 := nameLens.Set("Third")(nilStreet)
assert.NotNil(t, street3)
assert.Equal(t, "Third", street3.name)
}
func TestMakeLensRef_WithNilState_NestedStructure(t *testing.T) {
// Test with nested structure where inner can be nil
innerLens := MakeLensRef(
func(o *Outer) *Inner {
if o == nil {
return nil
}
return o.inner
},
func(o *Outer, i *Inner) *Outer {
o.inner = i
return o
},
)
var nilOuter *Outer = nil
// Get from nil outer
inner := innerLens.Get(nilOuter)
assert.Nil(t, inner)
// Set on nil outer
newInner := &Inner{Value: 42, Foo: "test"}
updated := innerLens.Set(newInner)(nilOuter)
assert.NotNil(t, updated)
assert.Equal(t, newInner, updated.inner)
}
func TestMakeLensWithEq_WithNilState(t *testing.T) {
// Test that MakeLensWithEq creates a total lens that works with nil pointers
nameLens := MakeLensWithEq(
EQ.FromStrictEquals[string](),
func(s *Street) string {
if s == nil {
return ""
}
return s.name
},
func(s *Street, name string) *Street {
s.name = name
return s
},
)
// Test Get with nil - should handle gracefully
var nilStreet *Street = nil
name := nameLens.Get(nilStreet)
assert.Equal(t, "", name)
// Test Set with nil - should create a new object with zero values except the set field
updated := nameLens.Set("NewStreet")(nilStreet)
assert.NotNil(t, updated)
assert.Equal(t, "NewStreet", updated.name)
assert.Equal(t, 0, updated.num) // Zero value for int
// Verify original nil pointer is unchanged
assert.Nil(t, nilStreet)
}
func TestMakeLensWithEq_WithNilState_IntField(t *testing.T) {
// Test with an int field lens with equality optimization
numLens := MakeLensWithEq(
EQ.FromStrictEquals[int](),
func(s *Street) int {
if s == nil {
return 0
}
return s.num
},
func(s *Street, num int) *Street {
s.num = num
return s
},
)
var nilStreet *Street = nil
// Get from nil should return zero value
num := numLens.Get(nilStreet)
assert.Equal(t, 0, num)
// Set on nil should create new object
updated := numLens.Set(42)(nilStreet)
assert.NotNil(t, updated)
assert.Equal(t, 42, updated.num)
assert.Equal(t, "", updated.name) // Zero value for string
}
func TestMakeLensWithEq_WithNilState_EqualityOptimization(t *testing.T) {
// Test that equality optimization works with nil state
nameLens := MakeLensWithEq(
EQ.FromStrictEquals[string](),
func(s *Street) string {
if s == nil {
return ""
}
return s.name
},
func(s *Street, name string) *Street {
s.name = name
return s
},
)
var nilStreet *Street = nil
// Setting empty string on nil should return a new object with empty string
// (since the zero value equals the set value)
updated1 := nameLens.Set("")(nilStreet)
assert.NotNil(t, updated1)
assert.Equal(t, "", updated1.name)
// Setting the same empty string again should return the same pointer (optimization)
updated2 := nameLens.Set("")(updated1)
assert.Same(t, updated1, updated2)
// Setting a different value should create a new copy
updated3 := nameLens.Set("Different")(updated1)
assert.NotSame(t, updated1, updated3)
assert.Equal(t, "Different", updated3.name)
assert.Equal(t, "", updated1.name)
}
func TestMakeLensWithEq_WithNilState_CustomEq(t *testing.T) {
// Test with custom equality predicate on nil state
customEq := EQ.FromEquals(func(a, b string) bool {
return len(a) == len(b) && a == b
})
nameLens := MakeLensWithEq(
customEq,
func(s *Street) string {
if s == nil {
return ""
}
return s.name
},
func(s *Street, name string) *Street {
s.name = name
return s
},
)
var nilStreet *Street = nil
// Get from nil
name := nameLens.Get(nilStreet)
assert.Equal(t, "", name)
// Set on nil with non-empty string
updated := nameLens.Set("Test")(nilStreet)
assert.NotNil(t, updated)
assert.Equal(t, "Test", updated.name)
// Set same value should return same pointer
same := nameLens.Set("Test")(updated)
assert.Same(t, updated, same)
}
func TestMakeLensWithEq_WithNilState_MultipleOperations(t *testing.T) {
// Test multiple operations on nil and non-nil states with equality optimization
nameLens := MakeLensWithEq(
EQ.FromStrictEquals[string](),
func(s *Street) string {
if s == nil {
return ""
}
return s.name
},
func(s *Street, name string) *Street {
s.name = name
return s
},
)
var nilStreet *Street = nil
// First operation on nil
street1 := nameLens.Set("First")(nilStreet)
assert.NotNil(t, street1)
assert.Equal(t, "First", street1.name)
// Second operation with same value - should return same pointer
street2 := nameLens.Set("First")(street1)
assert.Same(t, street1, street2)
// Third operation with different value - should create new copy
street3 := nameLens.Set("Second")(street2)
assert.NotSame(t, street2, street3)
assert.Equal(t, "Second", street3.name)
assert.Equal(t, "First", street2.name)
// Fourth operation back to nil with zero value
street4 := nameLens.Set("")(nilStreet)
assert.NotNil(t, street4)
assert.Equal(t, "", street4.name)
}

View File

@@ -22,7 +22,7 @@ import (
O "github.com/IBM/fp-go/v2/option"
)
func lensAsOptional[S, A any](creator func(get func(S) O.Option[A], set func(S, A) S) OPT.Optional[S, A], sa L.Lens[S, A]) OPT.Optional[S, A] {
func lensAsOptional[S, A any](creator func(get O.Kleisli[S, A], set func(S, A) S) OPT.Optional[S, A], sa L.Lens[S, A]) OPT.Optional[S, A] {
return creator(F.Flow2(sa.Get, O.Some[A]), func(s S, a A) S {
return sa.Set(a)(s)
})

View File

@@ -25,12 +25,32 @@ import (
O "github.com/IBM/fp-go/v2/option"
)
// Optional is an optional reference to a subpart of a data type
type Optional[S, A any] struct {
GetOption func(s S) O.Option[A]
Set func(a A) EM.Endomorphism[S]
name string
}
type (
// Optional is an optional reference to a subpart of a data type
Optional[S, A any] struct {
GetOption func(s S) O.Option[A]
Set func(a A) EM.Endomorphism[S]
name string
}
// Kleisli represents a function that takes a value of type A and returns an Optional[S, B].
// This is commonly used for composing optionals in a monadic style.
//
// Type Parameters:
// - S: The source type of the resulting optional
// - A: The input type to the function
// - B: The focus type of the resulting optional
Kleisli[S, A, B any] = func(A) Optional[S, B]
// Operator represents a function that transforms one optional into another.
// It takes an Optional[S, A] and returns an Optional[S, B], allowing for optional transformations.
//
// Type Parameters:
// - S: The source type (remains constant)
// - A: The original focus type
// - B: The new focus type
Operator[S, A, B any] = func(Optional[S, A]) Optional[S, B]
)
// setCopy wraps a setter for a pointer into a setter that first creates a copy before
// modifying that copy
@@ -46,11 +66,11 @@ func setCopy[SET ~func(*S, A) *S, S, A any](setter SET) func(s *S, a A) *S {
// and for other kinds of data structures that are copied by reference make sure the setter creates the copy.
//
//go:inline
func MakeOptional[S, A any](get func(S) O.Option[A], set func(S, A) S) Optional[S, A] {
func MakeOptional[S, A any](get O.Kleisli[S, A], set func(S, A) S) Optional[S, A] {
return MakeOptionalWithName(get, set, "GenericOptional")
}
func MakeOptionalWithName[S, A any](get func(S) O.Option[A], set func(S, A) S, name string) Optional[S, A] {
func MakeOptionalWithName[S, A any](get O.Kleisli[S, A], set func(S, A) S, name string) Optional[S, A] {
return Optional[S, A]{GetOption: get, Set: F.Bind2of2(set), name: name}
}
@@ -58,17 +78,17 @@ func MakeOptionalWithName[S, A any](get func(S) O.Option[A], set func(S, A) S, n
// copy, the implementation wraps the setter into one that copies the pointer before modifying it
//
//go:inline
func MakeOptionalRef[S, A any](get func(*S) O.Option[A], set func(*S, A) *S) Optional[*S, A] {
func MakeOptionalRef[S, A any](get O.Kleisli[*S, A], set func(*S, A) *S) Optional[*S, A] {
return MakeOptional(get, setCopy(set))
}
//go:inline
func MakeOptionalRefWithName[S, A any](get func(*S) O.Option[A], set func(*S, A) *S, name string) Optional[*S, A] {
func MakeOptionalRefWithName[S, A any](get O.Kleisli[*S, A], set func(*S, A) *S, name string) Optional[*S, A] {
return MakeOptionalWithName(get, setCopy(set), name)
}
// Id returns am optional implementing the identity operation
func idWithName[S any](creator func(get func(S) O.Option[S], set func(S, S) S, name string) Optional[S, S], name string) Optional[S, S] {
func idWithName[S any](creator func(get O.Kleisli[S, S], set func(S, S) S, name string) Optional[S, S], name string) Optional[S, S] {
return creator(O.Some[S], F.Second[S, S], name)
}
@@ -99,7 +119,7 @@ func optionalModify[S, A any](f func(A) A, optional Optional[S, A], s S) S {
}
// Compose combines two Optional and allows to narrow down the focus to a sub-Optional
func compose[S, A, B any](creator func(get func(S) O.Option[B], set func(S, B) S) Optional[S, B], ab Optional[A, B]) func(Optional[S, A]) Optional[S, B] {
func compose[S, A, B any](creator func(get O.Kleisli[S, B], set func(S, B) S) Optional[S, B], ab Optional[A, B]) Operator[S, A, B] {
abget := ab.GetOption
abset := ab.Set
return func(sa Optional[S, A]) Optional[S, B] {
@@ -114,17 +134,17 @@ func compose[S, A, B any](creator func(get func(S) O.Option[B], set func(S, B) S
}
// Compose combines two Optional and allows to narrow down the focus to a sub-Optional
func Compose[S, A, B any](ab Optional[A, B]) func(Optional[S, A]) Optional[S, B] {
func Compose[S, A, B any](ab Optional[A, B]) Operator[S, A, B] {
return compose(MakeOptional[S, B], ab)
}
// ComposeRef combines two Optional and allows to narrow down the focus to a sub-Optional
func ComposeRef[S, A, B any](ab Optional[A, B]) func(Optional[*S, A]) Optional[*S, B] {
func ComposeRef[S, A, B any](ab Optional[A, B]) Operator[*S, A, B] {
return compose(MakeOptionalRef[S, B], ab)
}
// fromPredicate implements the function generically for both the ref and the direct case
func fromPredicate[S, A any](creator func(get func(S) O.Option[A], set func(S, A) S) Optional[S, A], pred func(A) bool) func(func(S) A, func(S, A) S) Optional[S, A] {
func fromPredicate[S, A any](creator func(get O.Kleisli[S, A], set func(S, A) S) Optional[S, A], pred func(A) bool) func(func(S) A, func(S, A) S) Optional[S, A] {
fromPred := O.FromPredicate(pred)
return func(get func(S) A, set func(S, A) S) Optional[S, A] {
return creator(
@@ -163,21 +183,21 @@ func imap[S, A, B any](sa Optional[S, A], ab func(A) B, ba func(B) A) Optional[S
}
// IMap implements a bidirectional mapping of the transform
func IMap[S, A, B any](ab func(A) B, ba func(B) A) func(Optional[S, A]) Optional[S, B] {
func IMap[S, A, B any](ab func(A) B, ba func(B) A) Operator[S, A, B] {
return func(sa Optional[S, A]) Optional[S, B] {
return imap(sa, ab, ba)
}
}
func ModifyOption[S, A any](f func(A) A) func(Optional[S, A]) func(S) O.Option[S] {
return func(o Optional[S, A]) func(S) O.Option[S] {
func ModifyOption[S, A any](f func(A) A) func(Optional[S, A]) O.Kleisli[S, S] {
return func(o Optional[S, A]) O.Kleisli[S, S] {
return func(s S) O.Option[S] {
return optionalModifyOption(f, o, s)
}
}
}
func SetOption[S, A any](a A) func(Optional[S, A]) func(S) O.Option[S] {
func SetOption[S, A any](a A) func(Optional[S, A]) O.Kleisli[S, S] {
return ModifyOption[S](F.Constant1[A](a))
}
@@ -191,14 +211,14 @@ func ichain[S, A, B any](sa Optional[S, A], ab func(A) O.Option[B], ba func(B) O
}
// IChain implements a bidirectional mapping of the transform if the transform can produce optionals (e.g. in case of type mappings)
func IChain[S, A, B any](ab func(A) O.Option[B], ba func(B) O.Option[A]) func(Optional[S, A]) Optional[S, B] {
func IChain[S, A, B any](ab func(A) O.Option[B], ba func(B) O.Option[A]) Operator[S, A, B] {
return func(sa Optional[S, A]) Optional[S, B] {
return ichain(sa, ab, ba)
}
}
// IChainAny implements a bidirectional mapping to and from any
func IChainAny[S, A any]() func(Optional[S, any]) Optional[S, A] {
func IChainAny[S, A any]() Operator[S, any, A] {
fromAny := O.ToType[A]
toAny := O.ToAny[A]
return func(sa Optional[S, any]) Optional[S, A] {

View File

@@ -18,7 +18,6 @@ package prism
import (
"fmt"
EM "github.com/IBM/fp-go/v2/endomorphism"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
)
@@ -80,12 +79,12 @@ type (
// )
//
//go:inline
func MakePrism[S, A any](get func(S) Option[A], rev func(A) S) Prism[S, A] {
func MakePrism[S, A any](get O.Kleisli[S, A], rev func(A) S) Prism[S, A] {
return MakePrismWithName(get, rev, "GenericPrism")
}
//go:inline
func MakePrismWithName[S, A any](get func(S) Option[A], rev func(A) S, name string) Prism[S, A] {
func MakePrismWithName[S, A any](get O.Kleisli[S, A], rev func(A) S, name string) Prism[S, A] {
return Prism[S, A]{get, rev, name}
}
@@ -142,7 +141,7 @@ func FromPredicate[S any](pred func(S) bool) Prism[S, S] {
// outerPrism := MakePrism(...) // Prism[Outer, Inner]
// innerPrism := MakePrism(...) // Prism[Inner, Value]
// composed := Compose[Outer](innerPrism)(outerPrism) // Prism[Outer, Value]
func Compose[S, A, B any](ab Prism[A, B]) func(Prism[S, A]) Prism[S, B] {
func Compose[S, A, B any](ab Prism[A, B]) Operator[S, A, B] {
return func(sa Prism[S, A]) Prism[S, B] {
return MakePrismWithName(F.Flow2(
sa.GetOption,
@@ -159,7 +158,7 @@ func Compose[S, A, B any](ab Prism[A, B]) func(Prism[S, A]) Prism[S, B] {
// prismModifyOption applies a transformation function through a prism,
// returning Some(modified S) if the prism matches, None otherwise.
// This is an internal helper function.
func prismModifyOption[S, A any](f func(A) A, sa Prism[S, A], s S) Option[S] {
func prismModifyOption[S, A any](f Endomorphism[A], sa Prism[S, A], s S) Option[S] {
return F.Pipe2(
s,
sa.GetOption,
@@ -174,7 +173,7 @@ func prismModifyOption[S, A any](f func(A) A, sa Prism[S, A], s S) Option[S] {
// If the prism matches, it extracts the value, applies the function,
// and reconstructs the result. If the prism doesn't match, returns the original value.
// This is an internal helper function.
func prismModify[S, A any](f func(A) A, sa Prism[S, A], s S) S {
func prismModify[S, A any](f Endomorphism[A], sa Prism[S, A], s S) S {
return F.Pipe1(
prismModifyOption(f, sa, s),
O.GetOrElse(F.Constant(s)),
@@ -183,7 +182,7 @@ func prismModify[S, A any](f func(A) A, sa Prism[S, A], s S) S {
// prismSet is an internal helper that creates a setter function.
// Deprecated: Use Set instead.
func prismSet[S, A any](a A) func(Prism[S, A]) EM.Endomorphism[S] {
func prismSet[S, A any](a A) func(Prism[S, A]) Endomorphism[S] {
return F.Curry3(prismModify[S, A])(F.Constant1[A](a))
}
@@ -203,7 +202,7 @@ func prismSet[S, A any](a A) func(Prism[S, A]) EM.Endomorphism[S] {
// setter := Set[Option[int], int](100)
// result := setter(somePrism)(Some(42)) // Some(100)
// result = setter(somePrism)(None[int]()) // None[int]() (unchanged)
func Set[S, A any](a A) func(Prism[S, A]) EM.Endomorphism[S] {
func Set[S, A any](a A) func(Prism[S, A]) Endomorphism[S] {
return F.Curry3(prismModify[S, A])(F.Constant1[A](a))
}
@@ -271,7 +270,7 @@ func imap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](sa Prism[S, A], ab AB,
// func(n int) string { return strconv.Itoa(n) },
// func(s string) int { n, _ := strconv.Atoi(s); return n },
// )(intPrism) // Prism[Result, string]
func IMap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](ab AB, ba BA) func(Prism[S, A]) Prism[S, B] {
func IMap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](ab AB, ba BA) Operator[S, A, B] {
return func(sa Prism[S, A]) Prism[S, B] {
return imap(sa, ab, ba)
}

View File

@@ -321,6 +321,11 @@ func FromEither[E, T any]() Prism[Either[E, T], T] {
return MakePrismWithName(either.ToOption[E, T], either.Of[E, T], "PrismFromEither")
}
//go:inline
func FromResult[T any]() Prism[Result[T], T] {
return FromEither[error, T]()
}
// FromZero creates a prism that matches zero values of comparable types.
// It provides a safe way to work with zero values, handling non-zero values
// gracefully through the Option type.

View File

@@ -17,8 +17,10 @@ package prism
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/endomorphism"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
)
type (
@@ -95,5 +97,26 @@ type (
// - Prism composition for building complex error-handling pipelines
Either[E, T any] = either.Either[E, T]
Result[T any] = result.Result[T]
Endomorphism[T any] = endomorphism.Endomorphism[T]
Reader[R, T any] = reader.Reader[R, T]
// Kleisli represents a function that takes a value of type A and returns a Prism[S, B].
// This is commonly used for composing prisms in a monadic style.
//
// Type Parameters:
// - S: The source type of the resulting prism
// - A: The input type to the function
// - B: The focus type of the resulting prism
Kleisli[S, A, B any] = func(A) Prism[S, B]
// Operator represents a function that transforms one prism into another.
// It takes a Prism[S, A] and returns a Prism[S, B], allowing for prism transformations.
//
// Type Parameters:
// - S: The source type (remains constant)
// - A: The original focus type
// - B: The new focus type
Operator[S, A, B any] = func(Prism[S, A]) Prism[S, B]
)

View File

@@ -66,7 +66,7 @@ Creating a traversal for array elements:
doubled := F.Pipe2(
numbers,
TA.Traversal[int](),
T.Modify[[]int, int](func(n int) int { return n * 2 }),
T.Modify[[]int, int](N.Mul(2)),
)
// Result: [2, 4, 6, 8, 10]
@@ -85,7 +85,7 @@ The identity traversal focuses on the entire structure:
idTrav := T.Id[int, int]()
value := 42
result := T.Modify[int, int](func(n int) int { return n * 2 })(idTrav)(value)
result := T.Modify[int, int](N.Mul(2))(idTrav)(value)
// Result: 84
# Folding with Traversals
@@ -212,7 +212,7 @@ Traverse over the Right values:
doubled := F.Pipe2(
results,
allRightsTrav,
T.Modify[[]E.Either[string, int], int](func(n int) int { return n * 2 }),
T.Modify[[]E.Either[string, int], int](N.Mul(2)),
)
// Result: [Right(20), Left("error"), Right(40)]
@@ -247,7 +247,7 @@ Traverse over Some values:
incremented := F.Pipe2(
values,
allSomesTrav,
T.Modify[[]O.Option[int], int](func(n int) int { return n + 1 }),
T.Modify[[]O.Option[int], int](N.Add(1)),
)
// Result: [Some(2), None, Some(3), None, Some(4)]

View File

@@ -90,7 +90,7 @@ Curried versions for composition:
// Compose multiple transformations
transform := F.Flow2(
pair.MapHead[string](func(n int) int { return n * 2 }),
pair.MapHead[string](N.Mul(2)),
pair.MapTail[int](func(s string) int { return len(s) }),
)
result := transform(p) // Pair[int, int]{10, 5}

245
v2/readerio/logging.go Normal file
View File

@@ -0,0 +1,245 @@
// 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 (
"fmt"
"log"
"os"
"strings"
"sync"
"text/template"
)
// Logf constructs a logger function that can be used with ChainFirst or similar operations.
// The prefix string contains the format string for both the reader context (R) and the value (A).
// It uses log.Printf to output the formatted message.
//
// Type Parameters:
// - R: Reader context type
// - A: Value type
//
// Parameters:
// - prefix: Format string that accepts two arguments: the reader context and the value
//
// Returns:
// - A Kleisli arrow that logs the context and value, then returns the original value
//
// Example:
//
// type Config struct {
// AppName string
// }
// result := pipe.Pipe2(
// fetchUser(),
// readerio.ChainFirst(readerio.Logf[Config, User]("[%v] User: %+v")),
// processUser,
// )(Config{AppName: "MyApp"})()
func Logf[R, A any](prefix string) Kleisli[R, A, A] {
return func(a A) ReaderIO[R, A] {
return func(r R) IO[A] {
return func() A {
log.Printf(prefix, r, a)
return a
}
}
}
}
// Printf constructs a printer function that can be used with ChainFirst or similar operations.
// The prefix string contains the format string for both the reader context (R) and the value (A).
// Unlike Logf, this prints to stdout without log prefixes.
//
// Type Parameters:
// - R: Reader context type
// - A: Value type
//
// Parameters:
// - prefix: Format string that accepts two arguments: the reader context and the value
//
// Returns:
// - A Kleisli arrow that prints the context and value, then returns the original value
//
// Example:
//
// type Config struct {
// Debug bool
// }
// result := pipe.Pipe2(
// fetchData(),
// readerio.ChainFirst(readerio.Printf[Config, Data]("[%v] Data: %+v\n")),
// processData,
// )(Config{Debug: true})()
func Printf[R, A any](prefix string) Kleisli[R, A, A] {
return func(a A) ReaderIO[R, A] {
return func(r R) IO[A] {
return func() A {
fmt.Printf(prefix, r, a)
return a
}
}
}
}
// handleLoggingG is a generic 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 a context struct containing both the reader context (R) and value (A).
//
// 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 context and value
//
// The template is compiled lazily using sync.Once to ensure it's only parsed once.
// The template receives a context struct with fields R (reader context) and A (value).
// 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, any] {
var tmp *template.Template
var err error
var once sync.Once
type context struct {
R any
A any
}
init := func() {
tmp, err = template.New("").Parse(prefix)
}
return func(a any) ReaderIO[any, any] {
return func(r 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, context{r, a})
if tmpErr != nil {
onError(tmpErr)
onSuccess(fmt.Sprintf("%v: %v", r, a))
} else {
onSuccess(buffer.String())
}
} else {
onError(err)
onSuccess(fmt.Sprintf("%v: %v", r, a))
}
// in any case return the original value
return a
}
}
}
}
// handleLogging is a typed wrapper around handleLoggingG that creates a Kleisli arrow
// for logging/printing values using Go template syntax.
//
// 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 context and value
//
// Returns:
// - A Kleisli arrow that formats and outputs the value, then returns it unchanged
func handleLogging[R, A any](onSuccess func(string), onError func(error), prefix string) Kleisli[R, A, A] {
generic := handleLoggingG(onSuccess, onError, prefix)
return func(a A) ReaderIO[R, A] {
ga := generic(a)
return func(r R) IO[A] {
gr := ga(r)
return func() A {
gr()
return a
}
}
}
}
// LogGo constructs a logger function using Go template syntax for formatting.
// The prefix string is parsed as a Go template and executed with a context struct
// containing both the reader context (R) and the value (A) as fields .R and .A.
// Both successful output and template errors are logged using log.Println.
//
// Type Parameters:
// - R: Reader context type
// - A: Value type
//
// Parameters:
// - prefix: Go template string with access to .R (context) and .A (value)
//
// Returns:
// - A Kleisli arrow that logs the formatted output and returns the original value
//
// Example:
//
// type Config struct {
// AppName string
// }
// type User struct {
// Name string
// Age int
// }
// result := pipe.Pipe2(
// fetchUser(),
// readerio.ChainFirst(readerio.LogGo[Config, User]("[{{.R.AppName}}] User: {{.A.Name}}, Age: {{.A.Age}}")),
// processUser,
// )(Config{AppName: "MyApp"})()
func LogGo[R, A any](prefix string) Kleisli[R, A, A] {
return handleLogging[R, 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 a context struct
// containing both the reader context (R) and the value (A) as fields .R and .A.
// Successful output is printed to stdout using fmt.Println, while template errors
// are printed to stderr using fmt.Fprintln.
//
// Type Parameters:
// - R: Reader context type
// - A: Value type
//
// Parameters:
// - prefix: Go template string with access to .R (context) and .A (value)
//
// Returns:
// - A Kleisli arrow that prints the formatted output and returns the original value
//
// Example:
//
// type Config struct {
// Verbose bool
// }
// type Data struct {
// ID int
// Value string
// }
// result := pipe.Pipe2(
// fetchData(),
// readerio.ChainFirst(readerio.PrintGo[Config, Data]("{{if .R.Verbose}}[VERBOSE] {{end}}Data: {{.A.ID}} - {{.A.Value}}")),
// processData,
// )(Config{Verbose: true})()
func PrintGo[R, A any](prefix string) Kleisli[R, A, A] {
return handleLogging[R, A](func(value string) {
fmt.Println(value)
}, func(err error) {
fmt.Fprintln(os.Stderr, err)
}, prefix)
}

365
v2/readerio/logging_test.go Normal file
View File

@@ -0,0 +1,365 @@
// 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 (
"testing"
"github.com/stretchr/testify/assert"
)
type (
TestConfig struct {
AppName string
Debug bool
}
TestUser struct {
Name string
Age int
}
TestData struct {
ID int
Value string
}
)
func TestLogf(t *testing.T) {
l := Logf[TestConfig, int]("[%v] Value: %d")
rio := l(42)
config := TestConfig{AppName: "TestApp", Debug: true}
result := rio(config)()
assert.Equal(t, 42, result)
}
func TestLogfReturnsOriginalValue(t *testing.T) {
l := Logf[TestConfig, TestUser]("[%v] User: %+v")
rio := l(TestUser{Name: "Alice", Age: 30})
config := TestConfig{AppName: "TestApp"}
result := rio(config)()
assert.Equal(t, TestUser{Name: "Alice", Age: 30}, result)
}
func TestLogfWithDifferentTypes(t *testing.T) {
// Test with string value
l1 := Logf[string, string]("[%s] Message: %s")
rio1 := l1("hello")
result1 := rio1("context")()
assert.Equal(t, "hello", result1)
// Test with struct value
l2 := Logf[int, TestData]("[%d] Data: %+v")
rio2 := l2(TestData{ID: 123, Value: "test"})
result2 := rio2(999)()
assert.Equal(t, TestData{ID: 123, Value: "test"}, result2)
}
func TestPrintf(t *testing.T) {
l := Printf[TestConfig, int]("[%v] Value: %d\n")
rio := l(42)
config := TestConfig{AppName: "TestApp", Debug: false}
result := rio(config)()
assert.Equal(t, 42, result)
}
func TestPrintfReturnsOriginalValue(t *testing.T) {
l := Printf[TestConfig, TestUser]("[%v] User: %+v\n")
rio := l(TestUser{Name: "Bob", Age: 25})
config := TestConfig{AppName: "TestApp"}
result := rio(config)()
assert.Equal(t, TestUser{Name: "Bob", Age: 25}, result)
}
func TestPrintfWithDifferentTypes(t *testing.T) {
// Test with float value
l1 := Printf[string, float64]("[%s] Number: %.2f\n")
rio1 := l1(3.14159)
result1 := rio1("PI")()
assert.Equal(t, 3.14159, result1)
// Test with bool value
l2 := Printf[int, bool]("[%d] Flag: %v\n")
rio2 := l2(true)
result2 := rio2(1)()
assert.True(t, result2)
}
func TestLogGo(t *testing.T) {
l := LogGo[TestConfig, TestUser]("[{{.R.AppName}}] User: {{.A.Name}}, Age: {{.A.Age}}")
rio := l(TestUser{Name: "Charlie", Age: 35})
config := TestConfig{AppName: "MyApp", Debug: true}
result := rio(config)()
assert.Equal(t, TestUser{Name: "Charlie", Age: 35}, result)
}
func TestLogGoReturnsOriginalValue(t *testing.T) {
l := LogGo[TestConfig, TestData]("{{.R.AppName}}: Data {{.A.ID}} - {{.A.Value}}")
rio := l(TestData{ID: 456, Value: "test data"})
config := TestConfig{AppName: "TestApp"}
result := rio(config)()
assert.Equal(t, TestData{ID: 456, Value: "test data"}, result)
}
func TestLogGoWithInvalidTemplate(t *testing.T) {
// Invalid template syntax - should not panic
l := LogGo[TestConfig, int]("Value: {{.A.MissingField")
rio := l(42)
config := TestConfig{AppName: "TestApp"}
assert.NotPanics(t, func() {
result := rio(config)()
assert.Equal(t, 42, result)
})
}
func TestLogGoWithComplexTemplate(t *testing.T) {
type Address struct {
Street string
City string
}
type Person struct {
Name string
Address Address
}
l := LogGo[TestConfig, Person]("[{{.R.AppName}}] Person: {{.A.Name}} from {{.A.Address.City}}")
rio := l(Person{
Name: "David",
Address: Address{Street: "Main St", City: "NYC"},
})
config := TestConfig{AppName: "TestApp"}
result := rio(config)()
assert.Equal(t, "David", result.Name)
assert.Equal(t, "NYC", result.Address.City)
}
func TestLogGoWithConditionalTemplate(t *testing.T) {
l := LogGo[TestConfig, int]("{{if .R.Debug}}[DEBUG] {{end}}Value: {{.A}}")
rio := l(100)
config := TestConfig{AppName: "TestApp", Debug: true}
result := rio(config)()
assert.Equal(t, 100, result)
}
func TestPrintGo(t *testing.T) {
l := PrintGo[TestConfig, TestUser]("[{{.R.AppName}}] User: {{.A.Name}}, Age: {{.A.Age}}")
rio := l(TestUser{Name: "Eve", Age: 28})
config := TestConfig{AppName: "MyApp", Debug: false}
result := rio(config)()
assert.Equal(t, TestUser{Name: "Eve", Age: 28}, result)
}
func TestPrintGoReturnsOriginalValue(t *testing.T) {
l := PrintGo[TestConfig, TestData]("{{.R.AppName}}: {{.A.ID}} - {{.A.Value}}")
rio := l(TestData{ID: 789, Value: "sample"})
config := TestConfig{AppName: "TestApp"}
result := rio(config)()
assert.Equal(t, TestData{ID: 789, Value: "sample"}, result)
}
func TestPrintGoWithInvalidTemplate(t *testing.T) {
// Invalid template syntax - should not panic
l := PrintGo[TestConfig, string]("Value: {{.")
rio := l("test")
config := TestConfig{AppName: "TestApp"}
assert.NotPanics(t, func() {
result := rio(config)()
assert.Equal(t, "test", result)
})
}
func TestPrintGoWithComplexTemplate(t *testing.T) {
type Score struct {
Player string
Points int
}
l := PrintGo[TestConfig, Score]("{{if .R.Debug}}[DEBUG] {{end}}{{.A.Player}}: {{.A.Points}} points")
rio := l(Score{Player: "Alice", Points: 100})
config := TestConfig{AppName: "GameApp", Debug: true}
result := rio(config)()
assert.Equal(t, "Alice", result.Player)
assert.Equal(t, 100, result.Points)
}
func TestLogGoInPipeline(t *testing.T) {
config := TestConfig{AppName: "PipelineApp", Debug: true}
// Create a pipeline using Chain and logging
pipeline := MonadChain(
LogGo[TestConfig, TestData]("[{{.R.AppName}}] Processing: {{.A.ID}}")(TestData{ID: 10, Value: "initial"}),
func(d TestData) ReaderIO[TestConfig, TestData] {
return Of[TestConfig](TestData{ID: d.ID * 2, Value: d.Value + "_processed"})
},
)
result := pipeline(config)()
assert.Equal(t, 20, result.ID)
assert.Equal(t, "initial_processed", result.Value)
}
func TestPrintGoInPipeline(t *testing.T) {
config := TestConfig{AppName: "PrintApp", Debug: false}
pipeline := MonadChain(
PrintGo[TestConfig, string]("[{{.R.AppName}}] Input: {{.A}}")("hello"),
func(s string) ReaderIO[TestConfig, string] {
return Of[TestConfig](s + " world")
},
)
result := pipeline(config)()
assert.Equal(t, "hello world", result)
}
func TestLogfInPipeline(t *testing.T) {
config := TestConfig{AppName: "LogfApp"}
pipeline := MonadChain(
Logf[TestConfig, int]("[%v] Value: %d")(5),
func(n int) ReaderIO[TestConfig, int] {
return Of[TestConfig](n * 3)
},
)
result := pipeline(config)()
assert.Equal(t, 15, result)
}
func TestPrintfInPipeline(t *testing.T) {
config := TestConfig{AppName: "PrintfApp"}
pipeline := MonadChain(
Printf[TestConfig, float64]("[%v] Number: %.1f\n")(2.5),
func(n float64) ReaderIO[TestConfig, float64] {
return Of[TestConfig](n * 2)
},
)
result := pipeline(config)()
assert.Equal(t, 5.0, result)
}
func TestMultipleLoggersInPipeline(t *testing.T) {
config := TestConfig{AppName: "MultiApp", Debug: true}
pipeline := MonadChain(
Logf[TestConfig, int]("[%v] Initial: %d")(10),
func(n int) ReaderIO[TestConfig, int] {
return MonadChain(
LogGo[TestConfig, int]("[{{.R.AppName}}] After add: {{.A}}")(n+5),
func(n int) ReaderIO[TestConfig, int] {
return Of[TestConfig](n * 2)
},
)
},
)
result := pipeline(config)()
assert.Equal(t, 30, result)
}
func TestLogGoWithNestedStructs(t *testing.T) {
type Inner struct {
Value int
}
type Outer struct {
Name string
Inner Inner
}
l := LogGo[TestConfig, Outer]("[{{.R.AppName}}] {{.A.Name}}: {{.A.Inner.Value}}")
rio := l(Outer{Name: "Test", Inner: Inner{Value: 42}})
config := TestConfig{AppName: "NestedApp"}
result := rio(config)()
assert.Equal(t, "Test", result.Name)
assert.Equal(t, 42, result.Inner.Value)
}
func TestPrintGoWithNestedStructs(t *testing.T) {
type Config struct {
Host string
Port int
}
type Request struct {
Method string
Config Config
}
l := PrintGo[TestConfig, Request]("{{.A.Method}} -> {{.A.Config.Host}}:{{.A.Config.Port}}")
rio := l(Request{
Method: "GET",
Config: Config{Host: "localhost", Port: 8080},
})
config := TestConfig{AppName: "HTTPApp"}
result := rio(config)()
assert.Equal(t, "GET", result.Method)
assert.Equal(t, "localhost", result.Config.Host)
assert.Equal(t, 8080, result.Config.Port)
}
func TestLogGoWithEmptyTemplate(t *testing.T) {
l := LogGo[TestConfig, int]("")
rio := l(42)
config := TestConfig{AppName: "EmptyApp"}
result := rio(config)()
assert.Equal(t, 42, result)
}
func TestPrintGoWithEmptyTemplate(t *testing.T) {
l := PrintGo[TestConfig, string]("")
rio := l("test")
config := TestConfig{AppName: "EmptyApp"}
result := rio(config)()
assert.Equal(t, "test", result)
}

File diff suppressed because it is too large Load Diff

View File

@@ -21,11 +21,57 @@ import (
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/stretchr/testify/assert"
)
func TestMap(t *testing.T) {
type ReaderTestConfig struct {
Value int
Name string
}
func TestFromIO(t *testing.T) {
ioAction := G.Of(42)
rio := FromIO[ReaderTestConfig](ioAction)
config := ReaderTestConfig{Value: 10, Name: "test"}
result := rio(config)()
assert.Equal(t, 42, result)
}
func TestFromReader(t *testing.T) {
reader := func(config ReaderTestConfig) int {
return config.Value * 2
}
rio := FromReader(reader)
config := ReaderTestConfig{Value: 5, Name: "test"}
result := rio(config)()
assert.Equal(t, 10, result)
}
func TestOf(t *testing.T) {
rio := Of[ReaderTestConfig](100)
config := ReaderTestConfig{Value: 1, Name: "test"}
result := rio(config)()
assert.Equal(t, 100, result)
}
func TestMonadMap(t *testing.T) {
rio := Of[ReaderTestConfig](5)
doubled := MonadMap(rio, N.Mul(2))
config := ReaderTestConfig{Value: 1, Name: "test"}
result := doubled(config)()
assert.Equal(t, 10, result)
}
func TestMap(t *testing.T) {
g := F.Pipe1(
Of[context.Context](1),
Map[context.Context](utils.Double),
@@ -34,6 +80,37 @@ func TestMap(t *testing.T) {
assert.Equal(t, 2, g(context.Background())())
}
func TestMonadChain(t *testing.T) {
rio1 := Of[ReaderTestConfig](5)
result := MonadChain(rio1, func(n int) ReaderIO[ReaderTestConfig, int] {
return Of[ReaderTestConfig](n * 3)
})
config := ReaderTestConfig{Value: 1, Name: "test"}
assert.Equal(t, 15, result(config)())
}
func TestChain(t *testing.T) {
result := F.Pipe1(
Of[ReaderTestConfig](5),
Chain(func(n int) ReaderIO[ReaderTestConfig, int] {
return Of[ReaderTestConfig](n * 3)
}),
)
config := ReaderTestConfig{Value: 1, Name: "test"}
assert.Equal(t, 15, result(config)())
}
func TestMonadAp(t *testing.T) {
fabIO := Of[ReaderTestConfig](N.Mul(2))
faIO := Of[ReaderTestConfig](5)
result := MonadAp(fabIO, faIO)
config := ReaderTestConfig{Value: 1, Name: "test"}
assert.Equal(t, 10, result(config)())
}
func TestAp(t *testing.T) {
g := F.Pipe1(
Of[context.Context](utils.Double),
@@ -42,3 +119,448 @@ func TestAp(t *testing.T) {
assert.Equal(t, 2, g(context.Background())())
}
func TestMonadApSeq(t *testing.T) {
fabIO := Of[ReaderTestConfig](N.Add(10))
faIO := Of[ReaderTestConfig](5)
result := MonadApSeq(fabIO, faIO)
config := ReaderTestConfig{Value: 1, Name: "test"}
assert.Equal(t, 15, result(config)())
}
func TestMonadApPar(t *testing.T) {
fabIO := Of[ReaderTestConfig](N.Add(10))
faIO := Of[ReaderTestConfig](5)
result := MonadApPar(fabIO, faIO)
config := ReaderTestConfig{Value: 1, Name: "test"}
assert.Equal(t, 15, result(config)())
}
func TestAsk(t *testing.T) {
rio := Ask[ReaderTestConfig]()
config := ReaderTestConfig{Value: 42, Name: "test"}
result := rio(config)()
assert.Equal(t, config, result)
assert.Equal(t, 42, result.Value)
assert.Equal(t, "test", result.Name)
}
func TestAsks(t *testing.T) {
rio := Asks(func(c ReaderTestConfig) int {
return c.Value * 2
})
config := ReaderTestConfig{Value: 21, Name: "test"}
result := rio(config)()
assert.Equal(t, 42, result)
}
func TestMonadChainIOK(t *testing.T) {
rio := Of[ReaderTestConfig](5)
result := MonadChainIOK(rio, func(n int) G.IO[int] {
return G.Of(n * 4)
})
config := ReaderTestConfig{Value: 1, Name: "test"}
assert.Equal(t, 20, result(config)())
}
func TestChainIOK(t *testing.T) {
result := F.Pipe1(
Of[ReaderTestConfig](5),
ChainIOK[ReaderTestConfig, int, int](func(n int) G.IO[int] {
return G.Of(n * 4)
}),
)
config := ReaderTestConfig{Value: 1, Name: "test"}
assert.Equal(t, 20, result(config)())
}
func TestDefer(t *testing.T) {
counter := 0
rio := Defer(func() ReaderIO[ReaderTestConfig, int] {
counter++
return Of[ReaderTestConfig](counter)
})
config := ReaderTestConfig{Value: 1, Name: "test"}
result1 := rio(config)()
result2 := rio(config)()
assert.Equal(t, 1, result1)
assert.Equal(t, 2, result2)
}
func TestMemoize(t *testing.T) {
counter := 0
rio := Of[ReaderTestConfig](0)
memoized := Memoize(MonadMap(rio, func(int) int {
counter++
return counter
}))
config := ReaderTestConfig{Value: 1, Name: "test"}
result1 := memoized(config)()
result2 := memoized(config)()
assert.Equal(t, 1, result1)
assert.Equal(t, 1, result2) // Same value, memoized
}
func TestMemoizeWithDifferentContexts(t *testing.T) {
rio := Ask[ReaderTestConfig]()
memoized := Memoize(MonadMap(rio, func(c ReaderTestConfig) int {
return c.Value
}))
config1 := ReaderTestConfig{Value: 10, Name: "first"}
config2 := ReaderTestConfig{Value: 20, Name: "second"}
result1 := memoized(config1)()
result2 := memoized(config2)() // Should still return 10 (memoized from first call)
assert.Equal(t, 10, result1)
assert.Equal(t, 10, result2) // Memoized value from first context
}
func TestFlatten(t *testing.T) {
nested := Of[ReaderTestConfig](Of[ReaderTestConfig](42))
flattened := Flatten(nested)
config := ReaderTestConfig{Value: 1, Name: "test"}
result := flattened(config)()
assert.Equal(t, 42, result)
}
func TestMonadFlap(t *testing.T) {
fabIO := Of[ReaderTestConfig](N.Mul(3))
result := MonadFlap(fabIO, 7)
config := ReaderTestConfig{Value: 1, Name: "test"}
assert.Equal(t, 21, result(config)())
}
func TestFlap(t *testing.T) {
result := F.Pipe1(
Of[ReaderTestConfig](N.Mul(3)),
Flap[ReaderTestConfig, int, int](7),
)
config := ReaderTestConfig{Value: 1, Name: "test"}
assert.Equal(t, 21, result(config)())
}
func TestComplexPipeline(t *testing.T) {
// Test a complex pipeline combining multiple operations
result := F.Pipe3(
Ask[ReaderTestConfig](),
Map[ReaderTestConfig](func(c ReaderTestConfig) int { return c.Value }),
Chain(func(n int) ReaderIO[ReaderTestConfig, int] {
return Of[ReaderTestConfig](n * 2)
}),
Map[ReaderTestConfig](N.Add(10)),
)
config := ReaderTestConfig{Value: 5, Name: "test"}
assert.Equal(t, 20, result(config)()) // (5 * 2) + 10 = 20
}
func TestFromIOWithChain(t *testing.T) {
ioAction := G.Of(10)
result := F.Pipe1(
FromIO[ReaderTestConfig](ioAction),
Chain(func(n int) ReaderIO[ReaderTestConfig, int] {
return MonadMap(Ask[ReaderTestConfig](), func(c ReaderTestConfig) int {
return n + c.Value
})
}),
)
config := ReaderTestConfig{Value: 5, Name: "test"}
assert.Equal(t, 15, result(config)())
}
func TestFromReaderWithMap(t *testing.T) {
reader := func(c ReaderTestConfig) string {
return c.Name
}
result := F.Pipe1(
FromReader(reader),
Map[ReaderTestConfig](func(s string) string {
return s + " modified"
}),
)
config := ReaderTestConfig{Value: 1, Name: "original"}
assert.Equal(t, "original modified", result(config)())
}
func TestMonadMapTo(t *testing.T) {
rio := Of[ReaderTestConfig](42)
replaced := MonadMapTo(rio, "constant")
config := ReaderTestConfig{Value: 10, Name: "test"}
result := replaced(config)()
assert.Equal(t, "constant", result)
}
func TestMapTo(t *testing.T) {
result := F.Pipe1(
Of[ReaderTestConfig](42),
MapTo[ReaderTestConfig, int]("constant"),
)
config := ReaderTestConfig{Value: 10, Name: "test"}
assert.Equal(t, "constant", result(config)())
}
func TestMonadChainFirst(t *testing.T) {
sideEffect := 0
rio := Of[ReaderTestConfig](42)
result := MonadChainFirst(rio, func(n int) ReaderIO[ReaderTestConfig, string] {
sideEffect = n
return Of[ReaderTestConfig]("side effect")
})
config := ReaderTestConfig{Value: 1, Name: "test"}
value := result(config)()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestChainFirst(t *testing.T) {
sideEffect := 0
result := F.Pipe1(
Of[ReaderTestConfig](42),
ChainFirst(func(n int) ReaderIO[ReaderTestConfig, string] {
sideEffect = n
return Of[ReaderTestConfig]("side effect")
}),
)
config := ReaderTestConfig{Value: 1, Name: "test"}
value := result(config)()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestMonadTap(t *testing.T) {
sideEffect := 0
rio := Of[ReaderTestConfig](42)
result := MonadTap(rio, func(n int) ReaderIO[ReaderTestConfig, func()] {
sideEffect = n
return Of[ReaderTestConfig](func() {})
})
config := ReaderTestConfig{Value: 1, Name: "test"}
value := result(config)()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestTap(t *testing.T) {
sideEffect := 0
result := F.Pipe1(
Of[ReaderTestConfig](42),
Tap(func(n int) ReaderIO[ReaderTestConfig, func()] {
sideEffect = n
return Of[ReaderTestConfig](func() {})
}),
)
config := ReaderTestConfig{Value: 1, Name: "test"}
value := result(config)()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestMonadChainFirstIOK(t *testing.T) {
sideEffect := 0
rio := Of[ReaderTestConfig](42)
result := MonadChainFirstIOK(rio, func(n int) G.IO[string] {
sideEffect = n
return G.Of("side effect")
})
config := ReaderTestConfig{Value: 1, Name: "test"}
value := result(config)()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestChainFirstIOK(t *testing.T) {
sideEffect := 0
result := F.Pipe1(
Of[ReaderTestConfig](42),
ChainFirstIOK[ReaderTestConfig, int, string](func(n int) G.IO[string] {
sideEffect = n
return G.Of("side effect")
}),
)
config := ReaderTestConfig{Value: 1, Name: "test"}
value := result(config)()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestMonadTapIOK(t *testing.T) {
sideEffect := 0
rio := Of[ReaderTestConfig](42)
result := MonadTapIOK(rio, func(n int) G.IO[func()] {
sideEffect = n
return G.Of(func() {})
})
config := ReaderTestConfig{Value: 1, Name: "test"}
value := result(config)()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestTapIOK(t *testing.T) {
sideEffect := 0
result := F.Pipe1(
Of[ReaderTestConfig](42),
TapIOK[ReaderTestConfig, int, func()](func(n int) G.IO[func()] {
sideEffect = n
return G.Of(func() {})
}),
)
config := ReaderTestConfig{Value: 1, Name: "test"}
value := result(config)()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestMonadChainReaderK(t *testing.T) {
rio := Of[ReaderTestConfig](5)
result := MonadChainReaderK(rio, func(n int) func(ReaderTestConfig) int {
return func(c ReaderTestConfig) int { return n + c.Value }
})
config := ReaderTestConfig{Value: 10, Name: "test"}
assert.Equal(t, 15, result(config)())
}
func TestChainReaderK(t *testing.T) {
result := F.Pipe1(
Of[ReaderTestConfig](5),
ChainReaderK(func(n int) func(ReaderTestConfig) int {
return func(c ReaderTestConfig) int { return n + c.Value }
}),
)
config := ReaderTestConfig{Value: 10, Name: "test"}
assert.Equal(t, 15, result(config)())
}
func TestMonadChainFirstReaderK(t *testing.T) {
sideEffect := 0
rio := Of[ReaderTestConfig](42)
result := MonadChainFirstReaderK(rio, func(n int) func(ReaderTestConfig) string {
return func(c ReaderTestConfig) string {
sideEffect = n
return "side effect"
}
})
config := ReaderTestConfig{Value: 1, Name: "test"}
value := result(config)()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestChainFirstReaderK(t *testing.T) {
sideEffect := 0
result := F.Pipe1(
Of[ReaderTestConfig](42),
ChainFirstReaderK(func(n int) func(ReaderTestConfig) string {
return func(c ReaderTestConfig) string {
sideEffect = n
return "side effect"
}
}),
)
config := ReaderTestConfig{Value: 1, Name: "test"}
value := result(config)()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestMonadTapReaderK(t *testing.T) {
sideEffect := 0
rio := Of[ReaderTestConfig](42)
result := MonadTapReaderK(rio, func(n int) func(ReaderTestConfig) func() {
return func(c ReaderTestConfig) func() {
sideEffect = n
return func() {}
}
})
config := ReaderTestConfig{Value: 1, Name: "test"}
value := result(config)()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestTapReaderK(t *testing.T) {
sideEffect := 0
result := F.Pipe1(
Of[ReaderTestConfig](42),
TapReaderK(func(n int) func(ReaderTestConfig) func() {
return func(c ReaderTestConfig) func() {
sideEffect = n
return func() {}
}
}),
)
config := ReaderTestConfig{Value: 1, Name: "test"}
value := result(config)()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestRead(t *testing.T) {
rio := Of[ReaderTestConfig](42)
config := ReaderTestConfig{Value: 10, Name: "test"}
ioAction := Read[int](config)(rio)
result := ioAction()
assert.Equal(t, 42, result)
}
func TestTapWithLogging(t *testing.T) {
// Simulate logging scenario
logged := []int{}
result := F.Pipe3(
Of[ReaderTestConfig](42),
Tap(func(n int) ReaderIO[ReaderTestConfig, func()] {
logged = append(logged, n)
return Of[ReaderTestConfig](func() {})
}),
Map[ReaderTestConfig](N.Mul(2)),
Tap(func(n int) ReaderIO[ReaderTestConfig, func()] {
logged = append(logged, n)
return Of[ReaderTestConfig](func() {})
}),
)
config := ReaderTestConfig{Value: 1, Name: "test"}
value := result(config)()
assert.Equal(t, 84, value)
assert.Equal(t, []int{42, 84}, logged)
}

View File

@@ -21,10 +21,29 @@ import (
)
type (
IO[A any] = io.IO[A]
Reader[R, A any] = reader.Reader[R, A]
// IO represents a lazy computation that performs side effects and produces a value of type A.
// It's an alias for io.IO[A] and encapsulates effectful operations.
IO[A any] = io.IO[A]
// Reader represents a computation that depends on an environment of type R and produces a value of type A.
// It's an alias for reader.Reader[R, A] and is used for dependency injection patterns.
Reader[R, A any] = reader.Reader[R, A]
// ReaderIO combines Reader and IO monads. It represents a computation that:
// 1. Depends on an environment of type R (Reader aspect)
// 2. Performs side effects and produces a value of type A (IO aspect)
// This is useful for operations that need both dependency injection and effect management.
ReaderIO[R, A any] = Reader[R, IO[A]]
Kleisli[R, A, B any] = Reader[A, ReaderIO[R, B]]
// Kleisli represents a Kleisli arrow for the ReaderIO monad.
// It's a function from A to ReaderIO[R, B], which allows composition of
// monadic functions. This is the fundamental building block for chaining
// operations in the ReaderIO context.
Kleisli[R, A, B any] = Reader[A, ReaderIO[R, B]]
// Operator is a specialized Kleisli arrow that operates on ReaderIO values.
// It transforms a ReaderIO[R, A] into a ReaderIO[R, B], making it useful
// for building pipelines of ReaderIO operations. This is commonly used for
// middleware-style transformations and operation composition.
Operator[R, A, B any] = Kleisli[R, ReaderIO[R, A], B]
)

View File

@@ -18,6 +18,7 @@ package readerioresult
import (
"context"
"fmt"
"log"
"testing"
F "github.com/IBM/fp-go/v2/function"
@@ -57,3 +58,22 @@ func TestChainReaderK(t *testing.T) {
assert.Equal(t, result.Of("1"), g(context.Background())())
}
func TestTapReaderIOK(t *testing.T) {
rdr := Of[int]("TestTapReaderIOK")
x := F.Pipe1(
rdr,
TapReaderIOK(func(a string) ReaderIO[int, any] {
return func(ctx int) IO[any] {
return func() any {
log.Printf("Context: %d, Value: %s", ctx, a)
return nil
}
}
}),
)
x(10)()
}

View File

@@ -38,7 +38,7 @@ import (
// readeroption.Chain(readeroption.TraverseArray[DB](findUser)),
// )
// // result will be Some([]User) if all users are found, None otherwise
func TraverseArray[E, A, B any](f func(A) ReaderOption[E, B]) Kleisli[E, []A, []B] {
func TraverseArray[E, A, B any](f Kleisli[E, A, B]) Kleisli[E, []A, []B] {
return G.TraverseArray[ReaderOption[E, B], ReaderOption[E, []B], []A](f)
}

View File

@@ -86,7 +86,7 @@ func Do[R, S any](
func Bind[R, S1, S2, T any](
setter func(T) func(S1) S2,
f Kleisli[R, S1, T],
) func(ReaderOption[R, S1]) ReaderOption[R, S2] {
) Operator[R, S1, S2] {
return G.Bind[ReaderOption[R, S1], ReaderOption[R, S2]](setter, f)
}
@@ -94,7 +94,7 @@ func Bind[R, S1, S2, T any](
func Let[R, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) func(ReaderOption[R, S1]) ReaderOption[R, S2] {
) Operator[R, S1, S2] {
return G.Let[ReaderOption[R, S1], ReaderOption[R, S2]](setter, f)
}
@@ -102,14 +102,14 @@ func Let[R, S1, S2, T any](
func LetTo[R, S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) func(ReaderOption[R, S1]) ReaderOption[R, S2] {
) Operator[R, S1, S2] {
return G.LetTo[ReaderOption[R, S1], ReaderOption[R, S2]](setter, b)
}
// BindTo initializes a new state [S1] from a value [T]
func BindTo[R, S1, T any](
setter func(T) S1,
) func(ReaderOption[R, T]) ReaderOption[R, S1] {
) Operator[R, T, S1] {
return G.BindTo[ReaderOption[R, S1], ReaderOption[R, T]](setter)
}
@@ -157,7 +157,7 @@ func BindTo[R, S1, T any](
func ApS[R, S1, S2, T any](
setter func(T) func(S1) S2,
fa ReaderOption[R, T],
) func(ReaderOption[R, S1]) ReaderOption[R, S2] {
) Operator[R, S1, S2] {
return G.ApS[ReaderOption[R, S1], ReaderOption[R, S2]](setter, fa)
}
@@ -194,7 +194,7 @@ func ApS[R, S1, S2, T any](
func ApSL[R, S, T any](
lens L.Lens[S, T],
fa ReaderOption[R, T],
) func(ReaderOption[R, S]) ReaderOption[R, S] {
) Operator[R, S, S] {
return ApS(lens.Set, fa)
}
@@ -233,7 +233,7 @@ func ApSL[R, S, T any](
func BindL[R, S, T any](
lens L.Lens[S, T],
f Kleisli[R, T, T],
) func(ReaderOption[R, S]) ReaderOption[R, S] {
) Operator[R, S, S] {
return Bind(lens.Set, F.Flow2(lens.Get, f))
}
@@ -267,7 +267,7 @@ func BindL[R, S, T any](
func LetL[R, S, T any](
lens L.Lens[S, T],
f func(T) T,
) func(ReaderOption[R, S]) ReaderOption[R, S] {
) Operator[R, S, S] {
return Let[R](lens.Set, F.Flow2(lens.Get, f))
}
@@ -298,6 +298,6 @@ func LetL[R, S, T any](
func LetToL[R, S, T any](
lens L.Lens[S, T],
b T,
) func(ReaderOption[R, S]) ReaderOption[R, S] {
) Operator[R, S, S] {
return LetTo[R](lens.Set, b)
}

View File

@@ -89,7 +89,7 @@ func GetOrElse[GEA ~func(E) O.Option[A], GA ~func(E) A, E, A any](onNone func()
return optiont.GetOrElse(R.Chain[GEA, GA, E, O.Option[A], A], onNone, R.Of[GA, E, A])
}
func Ask[GEE ~func(E) O.Option[E], E, L any]() GEE {
func Ask[GEE ~func(E) O.Option[E], E any]() GEE {
return FR.Ask(FromReader[func(E) E, GEE, E, E])()
}

View File

@@ -170,7 +170,7 @@ func Ap[B, E, A any](fa ReaderOption[E, A]) Operator[E, func(A) B, B] {
// )
//
//go:inline
func FromPredicate[E, A any](pred func(A) bool) Kleisli[E, A, A] {
func FromPredicate[E, A any](pred Predicate[A]) Kleisli[E, A, A] {
return fromoption.FromPredicate(FromOption[E, A], pred)
}
@@ -186,11 +186,25 @@ func FromPredicate[E, A any](pred func(A) bool) Kleisli[E, A, A] {
// )(findUser(123))
//
//go:inline
func Fold[E, A, B any](onNone Reader[E, B], onRight func(A) Reader[E, B]) func(ReaderOption[E, A]) Reader[E, B] {
func Fold[E, A, B any](onNone Reader[E, B], onRight reader.Kleisli[E, A, B]) reader.Operator[E, Option[A], B] {
return optiont.MatchE(reader.Chain[E, Option[A], B], function.Constant(onNone), onRight)
}
func MonadFold[E, A, B any](fa ReaderOption[E, A], onNone Reader[E, B], onRight func(A) Reader[E, B]) Reader[E, B] {
// MonadFold extracts the value from a ReaderOption by providing handlers for both cases.
// This is the non-curried version of Fold.
// The onNone handler is called if the computation returns None.
// The onRight handler is called if the computation returns Some(a).
//
// Example:
//
// result := readeroption.MonadFold(
// findUser(123),
// reader.Of[Config]("not found"),
// func(user User) reader.Reader[Config, string] { return reader.Of[Config](user.Name) },
// )
//
//go:inline
func MonadFold[E, A, B any](fa ReaderOption[E, A], onNone Reader[E, B], onRight reader.Kleisli[E, A, B]) Reader[E, B] {
return optiont.MonadMatchE(fa, reader.MonadChain[E, Option[A], B], function.Constant(onNone), onRight)
}
@@ -203,7 +217,7 @@ func MonadFold[E, A, B any](fa ReaderOption[E, A], onNone Reader[E, B], onRight
// )(findUser(123))
//
//go:inline
func GetOrElse[E, A any](onNone Reader[E, A]) func(ReaderOption[E, A]) Reader[E, A] {
func GetOrElse[E, A any](onNone Reader[E, A]) reader.Operator[E, Option[A], A] {
return optiont.GetOrElse(reader.Chain[E, Option[A], A], function.Constant(onNone), reader.Of[E, A])
}
@@ -212,11 +226,11 @@ func GetOrElse[E, A any](onNone Reader[E, A]) func(ReaderOption[E, A]) Reader[E,
//
// Example:
//
// getConfig := readeroption.Ask[Config, any]()
// getConfig := readeroption.Ask[Config]()
// result := getConfig(myConfig) // Returns option.Some(myConfig)
//
//go:inline
func Ask[E, L any]() ReaderOption[E, E] {
func Ask[E any]() ReaderOption[E, E] {
return fromreader.Ask(FromReader[E, E])()
}
@@ -245,7 +259,7 @@ func Asks[E, A any](r Reader[E, A]) ReaderOption[E, A] {
// )
//
//go:inline
func MonadChainOptionK[E, A, B any](ma ReaderOption[E, A], f func(A) Option[B]) ReaderOption[E, B] {
func MonadChainOptionK[E, A, B any](ma ReaderOption[E, A], f O.Kleisli[A, B]) ReaderOption[E, B] {
return fromoption.MonadChainOptionK(
MonadChain[E, A, B],
FromOption[E, B],
@@ -266,7 +280,7 @@ func MonadChainOptionK[E, A, B any](ma ReaderOption[E, A], f func(A) Option[B])
// )
//
//go:inline
func ChainOptionK[E, A, B any](f func(A) Option[B]) Operator[E, A, B] {
func ChainOptionK[E, A, B any](f O.Kleisli[A, B]) Operator[E, A, B] {
return fromoption.ChainOptionK(
Chain[E, A, B],
FromOption[E, B],
@@ -339,11 +353,31 @@ func Flap[E, B, A any](a A) Operator[E, func(A) B, B] {
return functor.Flap(Map[E, func(A) B, B], a)
}
// MonadAlt provides an alternative ReaderOption if the first one returns None.
// If fa returns Some(a), that value is returned; otherwise, the alternative computation is executed.
// This is useful for providing fallback behavior.
//
// Example:
//
// primary := findUserInCache(123)
// fallback := findUserInDB(123)
// result := readeroption.MonadAlt(primary, fallback)
//
//go:inline
func MonadAlt[E, A any](fa ReaderOption[E, A], that ReaderOption[E, A]) ReaderOption[E, A] {
return MonadFold(fa, that, Of[E, A])
}
// Alt returns a function that provides an alternative ReaderOption if the first one returns None.
// This is the curried version of MonadAlt, useful for composition with F.Pipe.
//
// Example:
//
// result := F.Pipe1(
// findUserInCache(123),
// readeroption.Alt(findUserInDB(123)),
// )
//
//go:inline
func Alt[E, A any](that ReaderOption[E, A]) Operator[E, A, A] {
return Fold(that, Of[E, A])

View File

@@ -163,7 +163,7 @@ func TestGetOrElse(t *testing.T) {
}
func TestAsk(t *testing.T) {
ro := Ask[MyContext, any]()
ro := Ask[MyContext]()
result := ro(defaultContext)
assert.Equal(t, O.Of(defaultContext), result)
}

View File

@@ -65,6 +65,7 @@ package readeroption
import (
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/reader"
)
@@ -72,6 +73,8 @@ type (
// Lazy represents a deferred computation
Lazy[A any] = lazy.Lazy[A]
Predicate[A any] = predicate.Predicate[A]
// Option represents an optional value
Option[A any] = option.Option[A]

View File

@@ -13,6 +13,39 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package generic provides generic retry combinators that work with any monadic type.
//
// This package implements retry logic for monads that don't raise exceptions but
// signal failure in their type, such as Option, Either, and EitherT. The retry
// logic is parameterized over the monad operations, making it highly composable
// and reusable across different effect types.
//
// # Key Concepts
//
// The retry combinator takes:
// - A retry policy that determines when and how long to wait between retries
// - An action that produces a monadic value
// - A check function that determines if the result should be retried
//
// # Usage with Different Monads
//
// The generic retry function can be used with any monad by providing the
// appropriate monad operations (Chain, Of, and Delay). This allows the same
// retry logic to work with IO, IOEither, ReaderIO, and other monadic types.
//
// Example conceptual usage:
//
// // For IOEither[E, A]
// result := Retrying(
// IOE.Chain[E, A],
// IOE.Chain[E, R.RetryStatus],
// IOE.Of[E, A],
// IOE.Of[E, R.RetryStatus],
// IOE.Delay[E, R.RetryStatus],
// policy,
// action,
// shouldRetry,
// )
package generic
import (
@@ -23,9 +56,24 @@ import (
R "github.com/IBM/fp-go/v2/retry"
)
// Apply policy and delay by its amount if it results in a R.
// Returns updated status.
// HKTSTATUS = HKT<R.RetryStatus>
// applyAndDelay applies a retry policy to the current status and delays by the
// returned amount if the policy returns a delay. This is an internal helper
// function used by the Retrying combinator.
//
// The function:
// 1. Applies the policy to get the next status
// 2. If the policy returned a delay, waits for that duration
// 3. Returns the updated status wrapped in the monad
//
// Type parameters:
// - HKTSTATUS: The higher-kinded type representing the monadic status (e.g., IO[RetryStatus])
//
// Parameters:
// - monadOf: Lifts a RetryStatus into the monad
// - monadDelay: Delays execution by a duration within the monad
//
// Returns:
// - A function that takes a policy and status and returns the delayed status in the monad
func applyAndDelay[HKTSTATUS any](
monadOf func(R.RetryStatus) HKTSTATUS,
monadDelay func(time.Duration) func(HKTSTATUS) HKTSTATUS,
@@ -44,13 +92,60 @@ func applyAndDelay[HKTSTATUS any](
}
}
// Retry combinator for actions that don't raise exceptions, but
// signal in their type the outcome has failed. Examples are the
// `Option`, `Either` and `EitherT` monads.
// Retrying is a generic retry combinator for actions that don't raise exceptions,
// but signal failure in their type. This works with monads like Option, Either,
// and EitherT where the type itself indicates success or failure.
//
// policy - refers to the retry policy
// action - converts a status into an operation to be executed
// check - checks if the result of the action needs to be retried
// The function repeatedly executes an action until either:
// 1. The action succeeds (check returns false)
// 2. The retry policy returns None (retry limit reached)
// 3. The action fails in a way that shouldn't be retried
//
// Type parameters:
// - HKTA: The higher-kinded type for the action result (e.g., IO[A], Either[E, A])
// - HKTSTATUS: The higher-kinded type for the retry status (e.g., IO[RetryStatus])
// - A: The result type of the action
//
// Monad operations (first 5 parameters):
// - monadChain: Chains operations on HKTA (flatMap/bind for the result type)
// - monadChainStatus: Chains operations from HKTSTATUS to HKTA
// - monadOf: Lifts a value A into HKTA (pure/return for the result type)
// - monadOfStatus: Lifts a RetryStatus into HKTSTATUS
// - monadDelay: Delays execution by a duration within the monad
//
// Retry configuration (last 3 parameters):
// - policy: The retry policy that determines delays and limits
// - action: The action to retry, which receives the current RetryStatus
// - check: A predicate that returns true if the result should be retried
//
// Returns:
// - HKTA: The monadic result after retrying according to the policy
//
// Example conceptual usage with IOEither:
//
// policy := R.Monoid.Concat(
// R.LimitRetries(3),
// R.ExponentialBackoff(100*time.Millisecond),
// )
//
// action := func(status R.RetryStatus) IOEither[error, string] {
// return fetchData() // some IO operation that might fail
// }
//
// shouldRetry := func(result string) bool {
// return result == "" // retry if empty
// }
//
// result := Retrying(
// IOE.Chain[error, string],
// IOE.Chain[error, R.RetryStatus],
// IOE.Of[error, string],
// IOE.Of[error, R.RetryStatus],
// IOE.Delay[error, R.RetryStatus],
// policy,
// action,
// shouldRetry,
// )
func Retrying[HKTA, HKTSTATUS, A any](
monadChain func(func(A) HKTA) func(HKTA) HKTA,
monadChainStatus func(func(R.RetryStatus) HKTA) func(HKTSTATUS) HKTA,

View File

@@ -0,0 +1,375 @@
// 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 generic
import (
"testing"
"time"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
R "github.com/IBM/fp-go/v2/retry"
"github.com/stretchr/testify/assert"
)
// Mock monad for testing - represents a simple state monad that tracks execution
type TestMonad[A any] struct {
value A
delays []time.Duration
actionCalls int
}
func newTestMonad[A any](value A) *TestMonad[A] {
return &TestMonad[A]{
value: value,
delays: []time.Duration{},
actionCalls: 0,
}
}
// Monad operations for TestMonad
func testMonadOf[A any](value A) *TestMonad[A] {
return newTestMonad(value)
}
func testMonadChain[A, B any](f func(A) *TestMonad[B]) func(*TestMonad[A]) *TestMonad[B] {
return func(m *TestMonad[A]) *TestMonad[B] {
result := f(m.value)
// Preserve delays and action calls
result.delays = append(m.delays, result.delays...)
result.actionCalls = m.actionCalls + result.actionCalls
return result
}
}
func testMonadChainStatus[A any](f func(R.RetryStatus) *TestMonad[A]) func(*TestMonad[R.RetryStatus]) *TestMonad[A] {
return func(m *TestMonad[R.RetryStatus]) *TestMonad[A] {
result := f(m.value)
result.delays = append(m.delays, result.delays...)
result.actionCalls = m.actionCalls + result.actionCalls
return result
}
}
func testMonadDelay[A any](duration time.Duration) func(*TestMonad[A]) *TestMonad[A] {
return func(m *TestMonad[A]) *TestMonad[A] {
m.delays = append(m.delays, duration)
return m
}
}
func TestApplyAndDelay(t *testing.T) {
t.Run("applies policy and delays when policy returns Some", func(t *testing.T) {
policy := R.ConstantDelay(100 * time.Millisecond)
status := R.DefaultRetryStatus
applyDelay := applyAndDelay(testMonadOf[R.RetryStatus], testMonadDelay[R.RetryStatus])
result := applyDelay(policy, status)
assert.NotNil(t, result)
assert.Equal(t, 1, len(result.delays))
assert.Equal(t, 100*time.Millisecond, result.delays[0])
// Check that status was updated
assert.Equal(t, uint(1), result.value.IterNumber)
})
t.Run("does not delay when policy returns None", func(t *testing.T) {
policy := R.LimitRetries(0) // Always returns None
status := R.DefaultRetryStatus
applyDelay := applyAndDelay(testMonadOf[R.RetryStatus], testMonadDelay[R.RetryStatus])
result := applyDelay(policy, status)
assert.NotNil(t, result)
assert.Equal(t, 0, len(result.delays))
// Check that status was still updated
assert.Equal(t, uint(1), result.value.IterNumber)
})
t.Run("accumulates delays correctly", func(t *testing.T) {
policy := R.ExponentialBackoff(100 * time.Millisecond)
status := R.DefaultRetryStatus
applyDelay := applyAndDelay(testMonadOf[R.RetryStatus], testMonadDelay[R.RetryStatus])
// First application
result := applyDelay(policy, status)
assert.Equal(t, 1, len(result.delays))
assert.Equal(t, 100*time.Millisecond, result.delays[0])
// Second application
newStatus := result.value
result = applyDelay(policy, newStatus)
assert.Equal(t, 1, len(result.delays))
assert.Equal(t, 200*time.Millisecond, result.delays[0])
})
}
func TestRetrying(t *testing.T) {
t.Run("succeeds on first try when check returns false", func(t *testing.T) {
policy := R.LimitRetries(3)
callCount := 0
action := func(status R.RetryStatus) *TestMonad[string] {
callCount++
result := newTestMonad("success")
result.actionCalls = 1
return result
}
check := func(value string) bool {
return false // Don't retry
}
result := Retrying(
testMonadChain[string, string],
testMonadChainStatus[string],
testMonadOf[string],
testMonadOf[R.RetryStatus],
testMonadDelay[R.RetryStatus],
policy,
action,
check,
)
assert.Equal(t, "success", result.value)
assert.Equal(t, 1, callCount)
assert.Equal(t, 0, len(result.delays))
})
t.Run("retries until success", func(t *testing.T) {
policy := R.Monoid.Concat(
R.LimitRetries(5),
R.ConstantDelay(100*time.Millisecond),
)
callCount := 0
action := func(status R.RetryStatus) *TestMonad[int] {
callCount++
result := newTestMonad(callCount)
result.actionCalls = 1
return result
}
check := func(value int) bool {
return value < 3 // Retry until we reach 3
}
result := Retrying(
testMonadChain[int, int],
testMonadChainStatus[int],
testMonadOf[int],
testMonadOf[R.RetryStatus],
testMonadDelay[R.RetryStatus],
policy,
action,
check,
)
assert.Equal(t, 3, result.value)
assert.Equal(t, 3, callCount)
assert.Equal(t, 2, len(result.delays)) // 2 retries = 2 delays
assert.Equal(t, 100*time.Millisecond, result.delays[0])
assert.Equal(t, 100*time.Millisecond, result.delays[1])
})
t.Run("stops when retry limit is reached", func(t *testing.T) {
policy := R.Monoid.Concat(
R.LimitRetries(3),
R.ConstantDelay(50*time.Millisecond),
)
callCount := 0
action := func(status R.RetryStatus) *TestMonad[string] {
callCount++
result := newTestMonad("failure")
result.actionCalls = 1
return result
}
check := func(value string) bool {
return true // Always retry
}
result := Retrying(
testMonadChain[string, string],
testMonadChainStatus[string],
testMonadOf[string],
testMonadOf[R.RetryStatus],
testMonadDelay[R.RetryStatus],
policy,
action,
check,
)
assert.Equal(t, "failure", result.value)
assert.Equal(t, 4, callCount) // Initial + 3 retries
assert.Equal(t, 3, len(result.delays))
})
t.Run("respects exponential backoff", func(t *testing.T) {
policy := R.Monoid.Concat(
R.LimitRetries(4),
R.ExponentialBackoff(100*time.Millisecond),
)
callCount := 0
action := func(status R.RetryStatus) *TestMonad[int] {
callCount++
result := newTestMonad(callCount)
result.actionCalls = 1
return result
}
check := func(value int) bool {
return value < 4 // Retry until we reach 4
}
result := Retrying(
testMonadChain[int, int],
testMonadChainStatus[int],
testMonadOf[int],
testMonadOf[R.RetryStatus],
testMonadDelay[R.RetryStatus],
policy,
action,
check,
)
assert.Equal(t, 4, result.value)
assert.Equal(t, 4, callCount)
assert.Equal(t, 3, len(result.delays))
// Verify exponential growth: 100ms, 200ms, 400ms
assert.Equal(t, 100*time.Millisecond, result.delays[0])
assert.Equal(t, 200*time.Millisecond, result.delays[1])
assert.Equal(t, 400*time.Millisecond, result.delays[2])
})
t.Run("respects capped delay", func(t *testing.T) {
policy := R.Monoid.Concat(
R.LimitRetries(5),
R.CapDelay(300*time.Millisecond, R.ExponentialBackoff(100*time.Millisecond)),
)
callCount := 0
action := func(status R.RetryStatus) *TestMonad[int] {
callCount++
result := newTestMonad(callCount)
result.actionCalls = 1
return result
}
check := func(value int) bool {
return value < 5
}
result := Retrying(
testMonadChain[int, int],
testMonadChainStatus[int],
testMonadOf[int],
testMonadOf[R.RetryStatus],
testMonadDelay[R.RetryStatus],
policy,
action,
check,
)
assert.Equal(t, 5, result.value)
assert.Equal(t, 5, callCount)
assert.Equal(t, 4, len(result.delays))
// Verify capped growth: 100ms, 200ms, 300ms (capped), 300ms (capped)
assert.Equal(t, 100*time.Millisecond, result.delays[0])
assert.Equal(t, 200*time.Millisecond, result.delays[1])
assert.Equal(t, 300*time.Millisecond, result.delays[2])
assert.Equal(t, 300*time.Millisecond, result.delays[3])
})
t.Run("handles immediate success without delay", func(t *testing.T) {
policy := R.LimitRetries(3)
action := func(status R.RetryStatus) *TestMonad[int] {
result := newTestMonad(42)
result.actionCalls = 1
return result
}
check := func(value int) bool {
return false // Success on first try
}
result := Retrying(
testMonadChain[int, int],
testMonadChainStatus[int],
testMonadOf[int],
testMonadOf[R.RetryStatus],
testMonadDelay[R.RetryStatus],
policy,
action,
check,
)
assert.Equal(t, 42, result.value)
assert.Equal(t, 0, len(result.delays))
})
}
func TestRetryingWithOption(t *testing.T) {
t.Run("works with Option monad conceptually", func(t *testing.T) {
// This test demonstrates how Retrying would work with Option
// We use a simplified version since we can't easily test with real Option monad
policy := R.Monoid.Concat(
R.LimitRetries(3),
R.ConstantDelay(100*time.Millisecond),
)
attempts := 0
action := func(status R.RetryStatus) *TestMonad[O.Option[string]] {
attempts++
// Simulate Option: None on first 2 attempts, Some on 3rd
if attempts < 3 {
result := newTestMonad(O.None[string]())
result.actionCalls = 1
return result
}
result := newTestMonad(O.Some("success"))
result.actionCalls = 1
return result
}
check := func(value O.Option[string]) bool {
return O.IsNone(value) // Retry if None
}
result := Retrying(
testMonadChain[O.Option[string], O.Option[string]],
testMonadChainStatus[O.Option[string]],
testMonadOf[O.Option[string]],
testMonadOf[R.RetryStatus],
testMonadDelay[R.RetryStatus],
policy,
action,
check,
)
assert.True(t, O.IsSome(result.value))
assert.Equal(t, "success", O.GetOrElse(F.Constant(""))(result.value))
assert.Equal(t, 3, attempts)
assert.Equal(t, 2, len(result.delays))
})
}

View File

@@ -13,6 +13,42 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package retry provides functional retry policies and combinators for implementing
// retry logic with configurable backoff strategies.
//
// This package offers a composable approach to retrying operations, allowing you to:
// - Define retry policies that determine when and how long to wait between retries
// - Combine multiple policies using monoid operations
// - Implement various backoff strategies (constant, exponential, etc.)
// - Limit the number of retries or cap the maximum delay
//
// # Basic Usage
//
// Create a simple retry policy that retries up to 3 times with exponential backoff:
//
// policy := M.Concat(
// LimitRetries(3),
// ExponentialBackoff(100 * time.Millisecond),
// )(Monoid)
//
// # Retry Policies
//
// A RetryPolicy is a function that takes a RetryStatus and returns an optional delay.
// If the policy returns None, retrying stops. If it returns Some(delay), the operation
// will be retried after the specified delay.
//
// # Combining Policies
//
// Policies can be combined using the Monoid instance. When combining policies:
// - If either policy returns None, the combined policy returns None
// - If both return a delay, the larger delay is used
//
// Example combining a retry limit with exponential backoff:
//
// policy := M.Concat(
// LimitRetries(5),
// CapDelay(5*time.Second, ExponentialBackoff(100*time.Millisecond)),
// )(Monoid)
package retry
import (
@@ -25,64 +61,141 @@ import (
"github.com/IBM/fp-go/v2/ord"
)
// RetryStatus tracks the current state of a retry operation.
// It contains information about the iteration number, cumulative delay,
// and the delay from the previous attempt.
type RetryStatus struct {
// Iteration number, where `0` is the first try
// IterNumber is the iteration number, where 0 is the first try.
// This increments by 1 for each retry attempt.
IterNumber uint
// Delay incurred so far from retries
// CumulativeDelay is the total delay incurred so far from all retries.
// This is the sum of all previous delays.
CumulativeDelay time.Duration
// Latest attempt's delay. Will always be `none` on first run.
// PreviousDelay is the delay from the latest attempt.
// This will always be None on the first run (IterNumber == 0).
PreviousDelay Option[time.Duration]
}
// RetryPolicy is a function that takes an `RetryStatus` and
// possibly returns a delay in milliseconds. Iteration numbers start
// at zero and increase by one on each retry. A //None// return value from
// the function implies we have reached the retry limit.
// RetryPolicy is a function that takes a RetryStatus and possibly returns
// a delay duration. Iteration numbers start at zero and increase by one on
// each retry.
//
// A None return value from the policy indicates that the retry limit has been
// reached and no further retries should be attempted. A Some(duration) return
// value indicates that the operation should be retried after waiting for the
// specified duration.
//
// Example creating a custom policy:
//
// // Retry up to 3 times with a fixed 1 second delay
// customPolicy := func(status RetryStatus) Option[time.Duration] {
// if status.IterNumber < 3 {
// return O.Some(1 * time.Second)
// }
// return O.None[time.Duration]()
// }
type RetryPolicy = func(RetryStatus) Option[time.Duration]
const emptyDuration = time.Duration(0)
var ordDuration = ord.FromStrictCompare[time.Duration]()
// Monoid 'RetryPolicy' is a 'Monoid'. You can collapse multiple strategies into one using 'concat'.
// The semantics of this combination are as follows:
// IterNumber is an accessor function that extracts the iteration number
// from a RetryStatus. This is useful for functional composition.
//
// 1. If either policy returns 'None', the combined policy returns
// 'None'. This can be used to inhibit after a number of retries,
// for example.
// Example:
//
// 2. If both policies return a delay, the larger delay will be used.
// This is quite natural when combining multiple policies to achieve a
// certain effect.
// status := RetryStatus{IterNumber: 3, CumulativeDelay: 0, PreviousDelay: O.None[time.Duration]()}
// iter := IterNumber(status) // returns 3
func IterNumber(rs RetryStatus) uint {
return rs.IterNumber
}
// Monoid is the Monoid instance for RetryPolicy. You can collapse multiple
// retry strategies into one using the monoid's Concat operation.
//
// The semantics of this combination are:
//
// 1. If either policy returns None, the combined policy returns None.
// This allows you to limit retries by combining with LimitRetries.
//
// 2. If both policies return a delay, the larger delay will be used.
// This is natural when combining multiple policies to achieve a
// certain effect, such as exponential backoff with a cap.
//
// Example combining policies:
//
// // Retry up to 5 times with exponential backoff, capped at 10 seconds
// policy := M.Concat(
// M.Concat(
// LimitRetries(5),
// ExponentialBackoff(100*time.Millisecond),
// )(Monoid),
// CapDelay(10*time.Second, ConstantDelay(0)),
// )(Monoid)
var Monoid = M.FunctionMonoid[RetryStatus](O.ApplicativeMonoid(M.MakeMonoid(
ord.MaxSemigroup(ordDuration).Concat, emptyDuration)))
// LimitRetries retries immediately, but only up to `i` times.
// LimitRetries creates a retry policy that retries immediately (with zero delay),
// but only up to i times. After i retries, the policy returns None, stopping
// further retry attempts.
//
// The iteration count starts at 0, so LimitRetries(3) will allow the initial
// attempt plus 3 retries (4 total attempts).
//
// Example:
//
// // Allow up to 3 retries (4 total attempts)
// policy := LimitRetries(3)
//
// // Combine with a delay strategy
// policyWithDelay := M.Concat(
// LimitRetries(3),
// ConstantDelay(1*time.Second),
// )(Monoid)
func LimitRetries(i uint) RetryPolicy {
pred := func(value uint) bool {
return value < i
}
empty := F.Constant1[uint](emptyDuration)
return func(status RetryStatus) Option[time.Duration] {
return F.Pipe2(
status.IterNumber,
O.FromPredicate(pred),
O.Map(empty),
)
}
return F.Flow3(
IterNumber,
O.FromPredicate(func(value uint) bool {
return value < i
}),
O.Map(F.Constant1[uint](emptyDuration)),
)
}
// ConstantDelay delays with unlimited retries
// ConstantDelay creates a retry policy that always returns the same delay
// duration, allowing unlimited retries. This policy never returns None,
// so it should typically be combined with LimitRetries to prevent infinite retries.
//
// Example:
//
// // Retry with a constant 500ms delay, up to 5 times
// policy := M.Concat(
// LimitRetries(5),
// ConstantDelay(500*time.Millisecond),
// )(Monoid)
func ConstantDelay(delay time.Duration) RetryPolicy {
return F.Constant1[RetryStatus](O.Of(delay))
}
// CapDelay sets a time-upperbound for any delays that may be directed by the
// given policy. This function does not terminate the retrying. The policy
// capDelay(maxDelay, exponentialBackoff(n))` will never stop retrying. It
// will reach a state where it retries forever with a delay of `maxDelay`
// between each one. To get termination you need to use one of the
// 'limitRetries' function variants.
// CapDelay sets an upper bound on the delay returned by a retry policy.
// Any delay greater than maxDelay will be capped to maxDelay.
//
// This function does not terminate retrying. For example, the policy
// CapDelay(maxDelay, ExponentialBackoff(n)) will never stop retrying;
// it will reach a state where it retries forever with a delay of maxDelay
// between each attempt. To get termination, you need to combine this with
// LimitRetries or another limiting policy.
//
// Example:
//
// // Exponential backoff starting at 100ms, capped at 5 seconds, up to 10 retries
// policy := M.Concat(
// LimitRetries(10),
// CapDelay(5*time.Second, ExponentialBackoff(100*time.Millisecond)),
// )(Monoid)
func CapDelay(maxDelay time.Duration, policy RetryPolicy) RetryPolicy {
return F.Flow2(
policy,
@@ -90,16 +203,47 @@ func CapDelay(maxDelay time.Duration, policy RetryPolicy) RetryPolicy {
)
}
// ExponentialBackoff grows delay exponentially each iteration.
// Each delay will increase by a factor of two.
// ExponentialBackoff creates a retry policy where the delay grows exponentially
// with each iteration. Each delay increases by a factor of two.
//
// The delay for iteration n is: delay * 2^n
//
// For example, with an initial delay of 100ms:
// - Iteration 0: 100ms
// - Iteration 1: 200ms
// - Iteration 2: 400ms
// - Iteration 3: 800ms
// - etc.
//
// This policy never returns None, so it should be combined with LimitRetries
// and/or CapDelay to prevent unbounded delays.
//
// Example:
//
// // Exponential backoff starting at 100ms, capped at 10s, up to 5 retries
// policy := M.Concat(
// LimitRetries(5),
// CapDelay(10*time.Second, ExponentialBackoff(100*time.Millisecond)),
// )(Monoid)
func ExponentialBackoff(delay time.Duration) RetryPolicy {
return func(status RetryStatus) Option[time.Duration] {
return O.Some(delay * time.Duration(math.Pow(2, float64(status.IterNumber))))
}
}
// DefaultRetryStatus is the default retry status. Exported mostly to allow user code
// to test their handlers and retry policies.
// DefaultRetryStatus is the initial retry status used when starting a retry operation.
// It represents the state before any retries have been attempted:
// - IterNumber: 0 (first attempt)
// - CumulativeDelay: 0 (no delays yet)
// - PreviousDelay: None (no previous attempt)
//
// This is exported primarily to allow user code to test their retry handlers
// and retry policies.
//
// Example:
//
// policy := LimitRetries(3)
// result := policy(DefaultRetryStatus) // Returns Some(0) for immediate retry
var DefaultRetryStatus = RetryStatus{
IterNumber: 0,
CumulativeDelay: 0,
@@ -108,9 +252,27 @@ var DefaultRetryStatus = RetryStatus{
var getOrElseDelay = O.GetOrElse(F.Constant(emptyDuration))
/**
* Apply policy on status to see what the decision would be.
*/
// ApplyPolicy applies a retry policy to the current status and returns the
// updated status for the next iteration. This function:
// - Calls the policy with the current status to get the next delay
// - Increments the iteration number
// - Adds the delay to the cumulative delay
// - Stores the delay as the previous delay for the next iteration
//
// This is useful for testing policies or implementing custom retry logic.
//
// Example:
//
// policy := ExponentialBackoff(100 * time.Millisecond)
// status := DefaultRetryStatus
//
// // First retry
// status = ApplyPolicy(policy, status)
// // status.IterNumber == 1, status.PreviousDelay == Some(100ms)
//
// // Second retry
// status = ApplyPolicy(policy, status)
// // status.IterNumber == 2, status.PreviousDelay == Some(200ms)
func ApplyPolicy(policy RetryPolicy, status RetryStatus) RetryStatus {
previousDelay := policy(status)
return RetryStatus{

324
v2/retry/retry_test.go Normal file
View File

@@ -0,0 +1,324 @@
// 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 retry
import (
"testing"
"time"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
func TestIterNumber(t *testing.T) {
t.Run("returns iteration number from status", func(t *testing.T) {
status := RetryStatus{
IterNumber: 5,
CumulativeDelay: 1000 * time.Millisecond,
PreviousDelay: O.Some(500 * time.Millisecond),
}
assert.Equal(t, uint(5), IterNumber(status))
})
t.Run("returns zero for default status", func(t *testing.T) {
assert.Equal(t, uint(0), IterNumber(DefaultRetryStatus))
})
}
func TestLimitRetries(t *testing.T) {
t.Run("allows retries up to limit", func(t *testing.T) {
policy := LimitRetries(3)
// First attempt (iter 0)
result := policy(RetryStatus{IterNumber: 0, CumulativeDelay: 0, PreviousDelay: O.None[time.Duration]()})
assert.True(t, O.IsSome(result))
assert.Equal(t, time.Duration(0), O.GetOrElse(func() time.Duration { return -1 })(result))
// Second attempt (iter 1)
result = policy(RetryStatus{IterNumber: 1, CumulativeDelay: 0, PreviousDelay: O.Some(time.Duration(0))})
assert.True(t, O.IsSome(result))
// Third attempt (iter 2)
result = policy(RetryStatus{IterNumber: 2, CumulativeDelay: 0, PreviousDelay: O.Some(time.Duration(0))})
assert.True(t, O.IsSome(result))
// Fourth attempt (iter 3) - should fail
result = policy(RetryStatus{IterNumber: 3, CumulativeDelay: 0, PreviousDelay: O.Some(time.Duration(0))})
assert.True(t, O.IsNone(result))
})
t.Run("returns None when limit is reached", func(t *testing.T) {
policy := LimitRetries(2)
result := policy(RetryStatus{IterNumber: 2, CumulativeDelay: 0, PreviousDelay: O.None[time.Duration]()})
assert.True(t, O.IsNone(result))
})
t.Run("zero limit prevents all retries", func(t *testing.T) {
policy := LimitRetries(0)
result := policy(DefaultRetryStatus)
assert.True(t, O.IsNone(result))
})
}
func TestConstantDelay(t *testing.T) {
t.Run("returns constant delay for any status", func(t *testing.T) {
delay := 500 * time.Millisecond
policy := ConstantDelay(delay)
// First attempt
result := policy(DefaultRetryStatus)
assert.True(t, O.IsSome(result))
assert.Equal(t, delay, O.GetOrElse(func() time.Duration { return 0 })(result))
// Later attempts
result = policy(RetryStatus{IterNumber: 10, CumulativeDelay: 5 * time.Second, PreviousDelay: O.Some(delay)})
assert.True(t, O.IsSome(result))
assert.Equal(t, delay, O.GetOrElse(func() time.Duration { return 0 })(result))
})
t.Run("works with zero delay", func(t *testing.T) {
policy := ConstantDelay(0)
result := policy(DefaultRetryStatus)
assert.True(t, O.IsSome(result))
assert.Equal(t, time.Duration(0), O.GetOrElse(func() time.Duration { return -1 })(result))
})
}
func TestExponentialBackoff(t *testing.T) {
t.Run("grows delay exponentially", func(t *testing.T) {
baseDelay := 100 * time.Millisecond
policy := ExponentialBackoff(baseDelay)
testCases := []struct {
iterNumber uint
expectedDelay time.Duration
}{
{0, 100 * time.Millisecond}, // 100 * 2^0
{1, 200 * time.Millisecond}, // 100 * 2^1
{2, 400 * time.Millisecond}, // 100 * 2^2
{3, 800 * time.Millisecond}, // 100 * 2^3
{4, 1600 * time.Millisecond}, // 100 * 2^4
}
for _, tc := range testCases {
status := RetryStatus{
IterNumber: tc.iterNumber,
CumulativeDelay: 0,
PreviousDelay: O.None[time.Duration](),
}
result := policy(status)
assert.True(t, O.IsSome(result))
assert.Equal(t, tc.expectedDelay, O.GetOrElse(func() time.Duration { return 0 })(result))
}
})
t.Run("never returns None", func(t *testing.T) {
policy := ExponentialBackoff(100 * time.Millisecond)
// Even with very high iteration numbers
result := policy(RetryStatus{IterNumber: 100, CumulativeDelay: 0, PreviousDelay: O.None[time.Duration]()})
assert.True(t, O.IsSome(result))
})
}
func TestCapDelay(t *testing.T) {
t.Run("caps delay at maximum", func(t *testing.T) {
maxDelay := 1 * time.Second
basePolicy := ExponentialBackoff(100 * time.Millisecond)
policy := CapDelay(maxDelay, basePolicy)
// Small iteration - should not be capped
result := policy(RetryStatus{IterNumber: 2, CumulativeDelay: 0, PreviousDelay: O.None[time.Duration]()})
assert.True(t, O.IsSome(result))
delay := O.GetOrElse(func() time.Duration { return 0 })(result)
assert.Equal(t, 400*time.Millisecond, delay)
// Large iteration - should be capped
result = policy(RetryStatus{IterNumber: 10, CumulativeDelay: 0, PreviousDelay: O.None[time.Duration]()})
assert.True(t, O.IsSome(result))
delay = O.GetOrElse(func() time.Duration { return 0 })(result)
assert.Equal(t, maxDelay, delay)
})
t.Run("preserves None from underlying policy", func(t *testing.T) {
maxDelay := 1 * time.Second
basePolicy := LimitRetries(2)
policy := CapDelay(maxDelay, basePolicy)
// Within limit
result := policy(RetryStatus{IterNumber: 1, CumulativeDelay: 0, PreviousDelay: O.None[time.Duration]()})
assert.True(t, O.IsSome(result))
// Beyond limit
result = policy(RetryStatus{IterNumber: 2, CumulativeDelay: 0, PreviousDelay: O.None[time.Duration]()})
assert.True(t, O.IsNone(result))
})
}
func TestApplyPolicy(t *testing.T) {
t.Run("increments iteration number", func(t *testing.T) {
policy := ConstantDelay(100 * time.Millisecond)
status := DefaultRetryStatus
newStatus := ApplyPolicy(policy, status)
assert.Equal(t, uint(1), newStatus.IterNumber)
newStatus = ApplyPolicy(policy, newStatus)
assert.Equal(t, uint(2), newStatus.IterNumber)
})
t.Run("accumulates delay", func(t *testing.T) {
policy := ConstantDelay(100 * time.Millisecond)
status := DefaultRetryStatus
newStatus := ApplyPolicy(policy, status)
assert.Equal(t, 100*time.Millisecond, newStatus.CumulativeDelay)
newStatus = ApplyPolicy(policy, newStatus)
assert.Equal(t, 200*time.Millisecond, newStatus.CumulativeDelay)
newStatus = ApplyPolicy(policy, newStatus)
assert.Equal(t, 300*time.Millisecond, newStatus.CumulativeDelay)
})
t.Run("stores previous delay", func(t *testing.T) {
policy := ExponentialBackoff(100 * time.Millisecond)
status := DefaultRetryStatus
newStatus := ApplyPolicy(policy, status)
assert.True(t, O.IsSome(newStatus.PreviousDelay))
assert.Equal(t, 100*time.Millisecond, O.GetOrElse(func() time.Duration { return 0 })(newStatus.PreviousDelay))
newStatus = ApplyPolicy(policy, newStatus)
assert.True(t, O.IsSome(newStatus.PreviousDelay))
assert.Equal(t, 200*time.Millisecond, O.GetOrElse(func() time.Duration { return 0 })(newStatus.PreviousDelay))
})
t.Run("handles None from policy", func(t *testing.T) {
policy := LimitRetries(1)
status := RetryStatus{IterNumber: 1, CumulativeDelay: 0, PreviousDelay: O.None[time.Duration]()}
newStatus := ApplyPolicy(policy, status)
assert.Equal(t, uint(2), newStatus.IterNumber)
assert.True(t, O.IsNone(newStatus.PreviousDelay))
assert.Equal(t, time.Duration(0), newStatus.CumulativeDelay)
})
}
func TestMonoidCombination(t *testing.T) {
t.Run("combines limit with constant delay", func(t *testing.T) {
policy := Monoid.Concat(
LimitRetries(3),
ConstantDelay(500*time.Millisecond),
)
// Within limit - should return delay
result := policy(RetryStatus{IterNumber: 0, CumulativeDelay: 0, PreviousDelay: O.None[time.Duration]()})
assert.True(t, O.IsSome(result))
assert.Equal(t, 500*time.Millisecond, O.GetOrElse(func() time.Duration { return 0 })(result))
result = policy(RetryStatus{IterNumber: 2, CumulativeDelay: 0, PreviousDelay: O.None[time.Duration]()})
assert.True(t, O.IsSome(result))
// Beyond limit - should return None
result = policy(RetryStatus{IterNumber: 3, CumulativeDelay: 0, PreviousDelay: O.None[time.Duration]()})
assert.True(t, O.IsNone(result))
})
t.Run("takes maximum delay when both return Some", func(t *testing.T) {
policy := Monoid.Concat(
ConstantDelay(100*time.Millisecond),
ConstantDelay(500*time.Millisecond),
)
result := policy(DefaultRetryStatus)
assert.True(t, O.IsSome(result))
assert.Equal(t, 500*time.Millisecond, O.GetOrElse(func() time.Duration { return 0 })(result))
})
t.Run("returns None if either policy returns None", func(t *testing.T) {
policy := Monoid.Concat(
LimitRetries(2),
ConstantDelay(500*time.Millisecond),
)
// Beyond limit
result := policy(RetryStatus{IterNumber: 2, CumulativeDelay: 0, PreviousDelay: O.None[time.Duration]()})
assert.True(t, O.IsNone(result))
})
t.Run("complex policy combination", func(t *testing.T) {
// Exponential backoff, capped at 5 seconds, up to 10 retries
policy := Monoid.Concat(
LimitRetries(10),
CapDelay(5*time.Second, ExponentialBackoff(100*time.Millisecond)),
)
// Early iterations - exponential growth
result := policy(RetryStatus{IterNumber: 2, CumulativeDelay: 0, PreviousDelay: O.None[time.Duration]()})
assert.True(t, O.IsSome(result))
assert.Equal(t, 400*time.Millisecond, O.GetOrElse(func() time.Duration { return 0 })(result))
// Later iterations - capped
result = policy(RetryStatus{IterNumber: 8, CumulativeDelay: 0, PreviousDelay: O.None[time.Duration]()})
assert.True(t, O.IsSome(result))
assert.Equal(t, 5*time.Second, O.GetOrElse(func() time.Duration { return 0 })(result))
// Beyond limit
result = policy(RetryStatus{IterNumber: 10, CumulativeDelay: 0, PreviousDelay: O.None[time.Duration]()})
assert.True(t, O.IsNone(result))
})
}
func TestDefaultRetryStatus(t *testing.T) {
t.Run("has correct initial values", func(t *testing.T) {
assert.Equal(t, uint(0), DefaultRetryStatus.IterNumber)
assert.Equal(t, time.Duration(0), DefaultRetryStatus.CumulativeDelay)
assert.True(t, O.IsNone(DefaultRetryStatus.PreviousDelay))
})
}
func TestRetryStatusProgression(t *testing.T) {
t.Run("simulates full retry sequence", func(t *testing.T) {
policy := Monoid.Concat(
LimitRetries(3),
ExponentialBackoff(100*time.Millisecond),
)
status := DefaultRetryStatus
// First retry
status = ApplyPolicy(policy, status)
assert.Equal(t, uint(1), status.IterNumber)
assert.Equal(t, 100*time.Millisecond, status.CumulativeDelay)
assert.True(t, O.IsSome(status.PreviousDelay))
// Second retry
status = ApplyPolicy(policy, status)
assert.Equal(t, uint(2), status.IterNumber)
assert.Equal(t, 300*time.Millisecond, status.CumulativeDelay) // 100 + 200
// Third retry
status = ApplyPolicy(policy, status)
assert.Equal(t, uint(3), status.IterNumber)
assert.Equal(t, 700*time.Millisecond, status.CumulativeDelay) // 100 + 200 + 400
// Fourth attempt - should stop
status = ApplyPolicy(policy, status)
assert.Equal(t, uint(4), status.IterNumber)
assert.True(t, O.IsNone(status.PreviousDelay))
assert.Equal(t, 700*time.Millisecond, status.CumulativeDelay) // No additional delay
})
}

View File

@@ -15,7 +15,9 @@
package retry
import "github.com/IBM/fp-go/v2/option"
import (
"github.com/IBM/fp-go/v2/option"
)
type (
Option[A any] = option.Option[A]

View File

@@ -25,9 +25,9 @@ import (
A "github.com/IBM/fp-go/v2/array"
R "github.com/IBM/fp-go/v2/context/readerioresult"
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
IO "github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/result"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/stretchr/testify/assert"
)
@@ -69,9 +69,9 @@ func TestMultipleHttpRequests(t *testing.T) {
R.Map(A.Size[PostItem]),
)
result := data(context.Background())
res := data(context.Background())
assert.Equal(t, E.Of[error](count), result())
assert.Equal(t, result.Of(count), res())
}
func heterogeneousHTTPRequests() ReaderIOResult[T.Tuple2[PostItem, CatFact]] {

View File

@@ -18,10 +18,10 @@ package match
import (
"fmt"
E "github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/errors"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
S "github.com/IBM/fp-go/v2/string"
)
@@ -37,13 +37,13 @@ var (
// func(Thing) Either[error, string]
getName = F.Flow2(
Thing.GetName,
E.FromPredicate(S.IsNonEmpty, errors.OnSome[string]("value [%s] is empty")),
result.FromPredicate(S.IsNonEmpty, errors.OnSome[string]("value [%s] is empty")),
)
// func(option.Option[Thing]) Either[error, string]
GetName = F.Flow2(
E.FromOption[Thing](errors.OnNone("value is none")),
E.Chain(getName),
result.FromOption[Thing](errors.OnNone("value is none")),
result.Chain(getName),
)
)
@@ -54,7 +54,7 @@ func ExampleEither_match() {
res := F.Pipe2(
oThing,
GetName,
E.Fold(S.Format[error]("failed with error %v"), S.Format[string]("get value %s")),
result.Fold(S.Format[error]("failed with error %v"), S.Format[string]("get value %s")),
)
fmt.Println(res)

View File

@@ -20,7 +20,6 @@ import (
"time"
A "github.com/IBM/fp-go/v2/array"
E "github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/errors"
F "github.com/IBM/fp-go/v2/function"
I "github.com/IBM/fp-go/v2/identity"
@@ -106,14 +105,14 @@ var (
)
// checkActive :: User -> Either error User
checkActive = E.FromPredicate(Chapter08User.isActive, F.Constant1[Chapter08User](fmt.Errorf("your account is not active")))
checkActive = R.FromPredicate(Chapter08User.isActive, F.Constant1[Chapter08User](fmt.Errorf("your account is not active")))
// validateUser :: (User -> Either String ()) -> User -> Either String User
validateUser = F.Curry2(func(validate func(Chapter08User) Result[any], user Chapter08User) Result[Chapter08User] {
return F.Pipe2(
user,
validate,
E.MapTo[error, any](user),
R.MapTo[any](user),
)
})
@@ -127,8 +126,10 @@ var (
}
)
func Withdraw(amount float32) func(account Account) Option[Account] {
// Withdraw creates a Kleisli arrow that attempts to withdraw an amount from an account.
// Returns Some(account) if sufficient balance, None otherwise.
// This demonstrates the Option Kleisli type in action.
func Withdraw(amount float32) O.Kleisli[Account, Account] {
return F.Flow3(
getBalance,
O.FromPredicate(ord.Geq(ordFloat32)(amount)),
@@ -150,9 +151,11 @@ func MakeUser(d string) User {
return User{BirthDate: d}
}
var parseDate = F.Bind1of2(E.Eitherize2(time.Parse))(time.DateOnly)
var parseDate = F.Bind1of2(R.Eitherize2(time.Parse))(time.DateOnly)
func GetAge(now time.Time) func(User) Result[float64] {
// GetAge creates a Result Kleisli arrow that calculates age in days from a User's birth date.
// This demonstrates the Result Kleisli type for computations that may fail.
func GetAge(now time.Time) R.Kleisli[User, float64] {
return F.Flow3(
getBirthDate,
parseDate,
@@ -191,7 +194,7 @@ func Example_getAge() {
zoltar := F.Flow3(
GetAge(now),
R.Map(fortune),
E.GetOrElse(errors.ToString),
R.GetOrElse(errors.ToString),
)
fmt.Println(zoltar(MakeUser("2005-12-12")))
@@ -245,7 +248,7 @@ func Example_solution08D() {
// // validateName :: User -> Either String ()
validateName := F.Flow3(
Chapter08User.getName,
E.FromPredicate(F.Flow2(
R.FromPredicate(F.Flow2(
S.Size,
ord.Gt(ord.FromStrictCompare[int]())(3),
), errors.OnSome[string]("Your name %s is larger than 3 characters")),

View File

@@ -21,12 +21,12 @@ import (
"regexp"
A "github.com/IBM/fp-go/v2/array"
E "github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/errors"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioresult"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
S "github.com/IBM/fp-go/v2/string"
)
@@ -116,7 +116,7 @@ var (
)
// validateEmail :: Email -> Either error Email
validateEmail = E.FromPredicate(Matches(regexp.MustCompile(`\S+@\S+\.\S+`)), errors.OnSome[string]("email %s is invalid"))
validateEmail = result.FromPredicate(Matches(regexp.MustCompile(`\S+@\S+\.\S+`)), errors.OnSome[string]("email %s is invalid"))
// emailBlast :: [Email] -> IO ()
emailBlast = F.Flow2(

View File

@@ -13,6 +13,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package mostlyadequate contains examples from the "Mostly Adequate Guide to Functional Programming"
// adapted to Go using fp-go. These examples demonstrate functional programming concepts in a practical way.
package mostlyadequate
import (
@@ -22,9 +24,64 @@ import (
"github.com/IBM/fp-go/v2/result"
)
// Type aliases for common monads used throughout the examples.
// These aliases simplify type signatures and make the code more readable.
// This pattern is recommended in fp-go v2 - define aliases once and use them throughout your codebase.
type (
Result[A any] = result.Result[A]
// Result represents a computation that may fail with an error.
// It's an alias for result.Result[A] which is equivalent to either.Either[error, A].
// Use this when you need error handling with a specific success type.
//
// Example:
// func divide(a, b int) Result[int] {
// if b == 0 {
// return result.Error[int](errors.New("division by zero"))
// }
// return result.Ok(a / b)
// }
Result[A any] = result.Result[A]
// IOOption represents a lazy computation that may not produce a value.
// It combines IO (lazy evaluation) with Option (optional values).
// Use this when you have side effects that might not return a value.
//
// Example:
// func readConfig() IOOption[Config] {
// return func() option.Option[Config] {
// // Read from file system (side effect)
// if fileExists {
// return option.Some(config)
// }
// return option.None[Config]()
// }
// }
IOOption[A any] = iooption.IOOption[A]
Option[A any] = option.Option[A]
// Option represents an optional value - either Some(value) or None.
// Use this instead of pointers or sentinel values to represent absence of a value.
//
// Example:
// func findUser(id int) Option[User] {
// if user, found := users[id]; found {
// return option.Some(user)
// }
// return option.None[User]()
// }
Option[A any] = option.Option[A]
// IOResult represents a lazy computation that may fail with an error.
// It combines IO (lazy evaluation) with Result (error handling).
// Use this for side effects that can fail, like file I/O or HTTP requests.
//
// Example:
// func readFile(path string) IOResult[[]byte] {
// return func() result.Result[[]byte] {
// data, err := os.ReadFile(path)
// if err != nil {
// return result.Error[[]byte](err)
// }
// return result.Ok(data)
// }
// }
IOResult[A any] = ioresult.IOResult[A]
)

View File

@@ -23,15 +23,42 @@ import (
)
type (
// Endomorphism represents a function from a type to itself (A -> A).
// It's an alias for endomorphism.Endomorphism[A] and is commonly used for
// state transformations and updates.
Endomorphism[A any] = endomorphism.Endomorphism[A]
Lens[S, A any] = lens.Lens[S, A]
// some type aliases
Reader[R, A any] = reader.Reader[R, A]
Pair[L, R any] = pair.Pair[L, R]
// State represents an operation on top of a current [State] that produces a value and a new [State]
// Lens represents a functional reference to a part of a data structure.
// It's an alias for lens.Lens[S, A] where S is the whole structure and A is the part.
// Lenses provide composable getters and setters for immutable data structures.
Lens[S, A any] = lens.Lens[S, A]
// Reader represents a computation that depends on an environment of type R and produces a value of type A.
// It's an alias for reader.Reader[R, A] and is used for dependency injection patterns.
Reader[R, A any] = reader.Reader[R, A]
// Pair represents a tuple of two values of types L and R.
// It's an alias for pair.Pair[L, R] where L is the head (left) and R is the tail (right).
Pair[L, R any] = pair.Pair[L, R]
// State represents a stateful computation that takes an initial state of type S,
// performs some operation, and returns both a new state and a value of type A.
// It's defined as Reader[S, Pair[S, A]], meaning it's a function that:
// 1. Takes an initial state S as input
// 2. Returns a Pair where the head is the new state S and the tail is the computed value A
// The new state is in the head position and the value in the tail position because
// the pair monad operates on the tail, allowing monadic operations to transform
// the computed value while threading the state through the computation.
State[S, A any] = Reader[S, pair.Pair[S, A]]
Kleisli[S, A, B any] = Reader[A, State[S, B]]
// Kleisli represents a Kleisli arrow for the State monad.
// It's a function from A to State[S, B], which allows composition of
// stateful computations. This is the fundamental building block for chaining
// operations that both depend on and modify state.
Kleisli[S, A, B any] = Reader[A, State[S, B]]
// Operator is a specialized Kleisli arrow that operates on State values.
// It transforms a State[S, A] into a State[S, B], making it useful for
// building pipelines of stateful transformations while maintaining the state type S.
Operator[S, A, B any] = Kleisli[S, State[S, A], B]
)

View File

@@ -21,6 +21,7 @@ import (
"testing"
"github.com/IBM/fp-go/v2/number"
N "github.com/IBM/fp-go/v2/number"
O "github.com/IBM/fp-go/v2/ord"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
@@ -144,9 +145,9 @@ func TestMap2(t *testing.T) {
func TestMap3(t *testing.T) {
t3 := MakeTuple3(1, 2, 3)
mapper := Map3(
func(n int) int { return n * 2 },
func(n int) int { return n * 3 },
func(n int) int { return n * 4 },
N.Mul(2),
N.Mul(3),
N.Mul(4),
)
result := mapper(t3)
assert.Equal(t, MakeTuple3(2, 6, 12), result)
@@ -430,10 +431,10 @@ func TestTupled5Untupled5(t *testing.T) {
func TestMap4(t *testing.T) {
t4 := MakeTuple4(1, 2, 3, 4)
mapper := Map4(
func(n int) int { return n * 2 },
func(n int) int { return n * 3 },
func(n int) int { return n * 4 },
func(n int) int { return n * 5 },
N.Mul(2),
N.Mul(3),
N.Mul(4),
N.Mul(5),
)
result := mapper(t4)
assert.Equal(t, MakeTuple4(2, 6, 12, 20), result)
@@ -442,11 +443,11 @@ func TestMap4(t *testing.T) {
func TestMap5(t *testing.T) {
t5 := MakeTuple5(1, 2, 3, 4, 5)
mapper := Map5(
func(n int) int { return n + 1 },
N.Add(1),
func(n int) int { return n + 2 },
func(n int) int { return n + 3 },
func(n int) int { return n + 4 },
func(n int) int { return n + 5 },
N.Add(5),
)
result := mapper(t5)
assert.Equal(t, MakeTuple5(2, 4, 6, 8, 10), result)
@@ -679,7 +680,7 @@ func TestMap6(t *testing.T) {
func(n int) int { return n + 2 },
func(n int) int { return n + 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 },
)
result := mapper(t6)
@@ -689,13 +690,13 @@ func TestMap6(t *testing.T) {
func TestMap7(t *testing.T) {
t7 := MakeTuple7(1, 2, 3, 4, 5, 6, 7)
mapper := Map7(
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),
)
result := mapper(t7)
assert.Equal(t, MakeTuple7(2, 4, 6, 8, 10, 12, 14), result)
@@ -704,14 +705,14 @@ func TestMap7(t *testing.T) {
func TestMap8(t *testing.T) {
t8 := MakeTuple8(1, 2, 3, 4, 5, 6, 7, 8)
mapper := Map8(
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),
)
result := mapper(t8)
assert.Equal(t, MakeTuple8(2, 4, 6, 8, 10, 12, 14, 16), result)
@@ -720,15 +721,15 @@ func TestMap8(t *testing.T) {
func TestMap9(t *testing.T) {
t9 := MakeTuple9(1, 2, 3, 4, 5, 6, 7, 8, 9)
mapper := Map9(
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),
)
result := mapper(t9)
assert.Equal(t, MakeTuple9(2, 4, 6, 8, 10, 12, 14, 16, 18), result)
@@ -737,16 +738,16 @@ func TestMap9(t *testing.T) {
func TestMap10(t *testing.T) {
t10 := MakeTuple10(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
mapper := Map10(
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),
)
result := mapper(t10)
assert.Equal(t, MakeTuple10(2, 4, 6, 8, 10, 12, 14, 16, 18, 20), result)
@@ -1333,17 +1334,17 @@ func TestTupled15Untupled15(t *testing.T) {
func TestMap11(t *testing.T) {
t11 := MakeTuple11(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
mapper := Map11(
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 },
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),
N.Mul(2),
)
result := mapper(t11)
assert.Equal(t, MakeTuple11(2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22), result)
@@ -1352,18 +1353,18 @@ func TestMap11(t *testing.T) {
func TestMap12(t *testing.T) {
t12 := MakeTuple12(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
mapper := Map12(
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 },
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),
N.Mul(2),
N.Mul(2),
)
result := mapper(t12)
assert.Equal(t, MakeTuple12(2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24), result)
@@ -1372,19 +1373,19 @@ func TestMap12(t *testing.T) {
func TestMap13(t *testing.T) {
t13 := MakeTuple13(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13)
mapper := Map13(
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 },
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),
N.Mul(2),
N.Mul(2),
N.Mul(2),
)
result := mapper(t13)
assert.Equal(t, MakeTuple13(2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26), result)
@@ -1393,20 +1394,20 @@ func TestMap13(t *testing.T) {
func TestMap14(t *testing.T) {
t14 := MakeTuple14(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14)
mapper := Map14(
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 },
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),
N.Mul(2),
N.Mul(2),
N.Mul(2),
N.Mul(2),
)
result := mapper(t14)
assert.Equal(t, MakeTuple14(2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28), result)
@@ -1415,21 +1416,21 @@ func TestMap14(t *testing.T) {
func TestMap15(t *testing.T) {
t15 := MakeTuple15(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)
mapper := Map15(
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 },
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),
N.Mul(2),
N.Mul(2),
N.Mul(2),
N.Mul(2),
N.Mul(2),
)
result := mapper(t15)
assert.Equal(t, MakeTuple15(2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30), result)