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

Compare commits

...

2 Commits

Author SHA1 Message Date
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
41 changed files with 4038 additions and 361 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 {

237
v2/assert/example_test.go Normal file
View File

@@ -0,0 +1,237 @@
// 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")
}
// Made with Bob

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,441 @@
// 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)
}
}
// Made with Bob

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

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

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

View File

@@ -363,5 +363,3 @@ func TestPrintGoWithEmptyTemplate(t *testing.T) {
assert.Equal(t, "test", result)
}
// Made with Bob

View File

@@ -95,6 +95,7 @@ import (
"sync"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/chain"
"github.com/IBM/fp-go/v2/internal/fromio"
"github.com/IBM/fp-go/v2/internal/fromreader"
"github.com/IBM/fp-go/v2/internal/functor"
@@ -165,12 +166,36 @@ func FromReader[R, A any](r Reader[R, A]) ReaderIO[R, A] {
// Example:
//
// rio := readerio.Of[Config](5)
// doubled := readerio.MonadMap(rio, func(n int) int { return n * 2 })
// doubled := readerio.MonadMap(rio, N.Mul(2))
// result := doubled(config)() // Returns 10
func MonadMap[R, A, B any](fa ReaderIO[R, A], f func(A) B) ReaderIO[R, B] {
return readert.MonadMap[ReaderIO[R, A], ReaderIO[R, B]](io.MonadMap[A, B], fa, f)
}
// MonadMapTo replaces the value inside a ReaderIO with a constant value.
// This is useful when you want to discard the result of a computation but keep its effects.
//
// Type Parameters:
// - R: Reader environment type
// - A: Input value type (discarded)
// - B: Output value type
//
// Parameters:
// - fa: The ReaderIO whose value will be replaced
// - b: The constant value to use
//
// Returns:
// - A new ReaderIO with the constant value
//
// Example:
//
// rio := readerio.Of[Config](42)
// replaced := readerio.MonadMapTo(rio, "constant")
// result := replaced(config)() // Returns "constant"
func MonadMapTo[R, A, B any](fa ReaderIO[R, A], b B) ReaderIO[R, B] {
return MonadMap(fa, function.Constant1[A](b))
}
// Map creates a function that applies a transformation to a ReaderIO value.
// This is the curried version suitable for use in pipelines.
//
@@ -189,12 +214,36 @@ func MonadMap[R, A, B any](fa ReaderIO[R, A], f func(A) B) ReaderIO[R, B] {
//
// result := F.Pipe1(
// readerio.Of[Config](5),
// readerio.Map[Config](func(n int) int { return n * 2 }),
// readerio.Map[Config](N.Mul(2)),
// )(config)() // Returns 10
func Map[R, A, B any](f func(A) B) Operator[R, A, B] {
return readert.Map[ReaderIO[R, A], ReaderIO[R, B]](io.Map[A, B], f)
}
// MapTo creates a function that replaces a ReaderIO value with a constant.
// This is the curried version of [MonadMapTo], suitable for use in pipelines.
//
// Type Parameters:
// - R: Reader environment type
// - A: Input value type (discarded)
// - B: Output value type
//
// Parameters:
// - b: The constant value to use
//
// Returns:
// - An Operator that replaces ReaderIO values with the constant
//
// Example:
//
// result := F.Pipe1(
// readerio.Of[Config](42),
// readerio.MapTo[Config, int]("constant"),
// )(config)() // Returns "constant"
func MapTo[R, A, B any](b B) Operator[R, A, B] {
return Map[R](function.Constant1[A](b))
}
// MonadChain sequences two ReaderIO computations, where the second depends on the result of the first.
// This is the monadic bind operation for ReaderIO.
//
@@ -216,10 +265,70 @@ func Map[R, A, B any](f func(A) B) Operator[R, A, B] {
// result := readerio.MonadChain(rio1, func(n int) readerio.ReaderIO[Config, int] {
// return readerio.Of[Config](n * 2)
// })
func MonadChain[R, A, B any](ma ReaderIO[R, A], f func(A) ReaderIO[R, B]) ReaderIO[R, B] {
func MonadChain[R, A, B any](ma ReaderIO[R, A], f Kleisli[R, A, B]) ReaderIO[R, B] {
return readert.MonadChain(io.MonadChain[A, B], ma, f)
}
// MonadChainFirst sequences two ReaderIO computations but returns the result of the first.
// The second computation is executed for its side effects only (e.g., logging, validation).
//
// Type Parameters:
// - R: Reader environment type
// - A: Input and output value type
// - B: Intermediate value type (discarded)
//
// Parameters:
// - ma: The first ReaderIO computation
// - f: Function that produces the second ReaderIO (for side effects)
//
// Returns:
// - A ReaderIO with the result of the first computation
//
// Example:
//
// rio := readerio.Of[Config](42)
// result := readerio.MonadChainFirst(rio, func(n int) readerio.ReaderIO[Config, string] {
// // Log the value but don't change the result
// return readerio.Of[Config](fmt.Sprintf("Logged: %d", n))
// })
// value := result(config)() // Returns 42, but logging happened
func MonadChainFirst[R, A, B any](ma ReaderIO[R, A], f Kleisli[R, A, B]) ReaderIO[R, A] {
return chain.MonadChainFirst(
MonadChain,
MonadMap,
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.
//
// Type Parameters:
// - R: Reader environment type
// - A: Input and output value type
// - B: Side effect value type (discarded)
//
// 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
//
// Example:
//
// result := readerio.MonadTap(
// readerio.Of[Config](42),
// func(n int) readerio.ReaderIO[Config, func()] {
// return readerio.FromIO[Config](io.Of(func() { fmt.Println(n) }))
// },
// )
func MonadTap[R, A, B any](ma ReaderIO[R, A], f Kleisli[R, A, B]) ReaderIO[R, A] {
return MonadChainFirst(ma, f)
}
// Chain creates a function that sequences ReaderIO computations.
// This is the curried version suitable for use in pipelines.
//
@@ -242,10 +351,66 @@ func MonadChain[R, A, B any](ma ReaderIO[R, A], f func(A) ReaderIO[R, B]) Reader
// return readerio.Of[Config](n * 2)
// }),
// )(config)() // Returns 10
func Chain[R, A, B any](f func(A) ReaderIO[R, B]) Operator[R, A, B] {
func Chain[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, B] {
return readert.Chain[ReaderIO[R, A]](io.Chain[A, B], f)
}
// ChainFirst creates a function that sequences ReaderIO computations but returns the first result.
// This is the curried version of [MonadChainFirst], suitable for use in pipelines.
//
// Type Parameters:
// - R: Reader environment type
// - A: Input and output value type
// - B: Intermediate value type (discarded)
//
// Parameters:
// - f: Function that produces a side-effect ReaderIO
//
// Returns:
// - An Operator that sequences computations while preserving the original value
//
// Example:
//
// result := F.Pipe1(
// readerio.Of[Config](42),
// readerio.ChainFirst(func(n int) readerio.ReaderIO[Config, string] {
// return readerio.Of[Config](fmt.Sprintf("Logged: %d", n))
// }),
// )(config)() // Returns 42
func ChainFirst[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, A] {
return chain.ChainFirst(
Chain[R, A, A],
Map[R, B, A],
f,
)
}
// Tap creates a function that executes a side-effect computation but returns the original value.
// This is the curried version of [MonadTap], an alias for [ChainFirst].
//
// Type Parameters:
// - R: Reader environment type
// - A: Input and output value type
// - B: Side effect value type (discarded)
//
// Parameters:
// - f: Function that produces a side-effect ReaderIO
//
// Returns:
// - An Operator that taps ReaderIO computations
//
// Example:
//
// result := F.Pipe1(
// readerio.Of[Config](42),
// readerio.Tap(func(n int) readerio.ReaderIO[Config, func()] {
// return readerio.FromIO[Config](io.Of(func() { fmt.Println(n) }))
// }),
// )(config)() // Returns 42, prints 42
func Tap[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, A] {
return ChainFirst(f)
}
// Of creates a ReaderIO that returns a pure value, ignoring the environment.
// This is the monadic return/pure operation for ReaderIO.
//
@@ -284,7 +449,8 @@ func Of[R, A any](a A) ReaderIO[R, A] {
//
// Example:
//
// fabIO := readerio.Of[Config](func(n int) int { return n * 2 })
// fabIO := readerio.Of[Config](N.Mul(2))
//
// faIO := readerio.Of[Config](5)
// result := readerio.MonadAp(fabIO, faIO)(config)() // Returns 10
func MonadAp[B, R, A any](fab ReaderIO[R, func(A) B], fa ReaderIO[R, A]) ReaderIO[R, B] {
@@ -342,7 +508,7 @@ func MonadApPar[B, R, A any](fab ReaderIO[R, func(A) B], fa ReaderIO[R, A]) Read
// Example:
//
// result := F.Pipe1(
// readerio.Of[Config](func(n int) int { return n * 2 }),
// readerio.Of[Config](N.Mul(2)),
// readerio.Ap[int](readerio.Of[Config](5)),
// )(config)() // Returns 10
func Ap[B, R, A any](fa ReaderIO[R, A]) Operator[R, func(A) B, B] {
@@ -413,7 +579,7 @@ func Asks[R, A any](r Reader[R, A]) ReaderIO[R, A] {
// result := readerio.MonadChainIOK(rio, func(n int) io.IO[int] {
// return io.Of(n * 2)
// })
func MonadChainIOK[R, A, B any](ma ReaderIO[R, A], f func(A) IO[B]) ReaderIO[R, B] {
func MonadChainIOK[R, A, B any](ma ReaderIO[R, A], f io.Kleisli[A, B]) ReaderIO[R, B] {
return fromio.MonadChainIOK(
MonadChain[R, A, B],
FromIO[R, B],
@@ -421,6 +587,64 @@ func MonadChainIOK[R, A, B any](ma ReaderIO[R, A], f func(A) IO[B]) ReaderIO[R,
)
}
// MonadChainFirstIOK chains a ReaderIO with an IO-returning function but keeps the original value.
// The IO computation is executed for its side effects only.
//
// Type Parameters:
// - R: Reader environment type
// - A: Input and output value type
// - B: IO result type (discarded)
//
// Parameters:
// - ma: The ReaderIO computation
// - f: Function that takes a value and returns an IO
//
// Returns:
// - A ReaderIO with the original value after executing the IO
//
// Example:
//
// rio := readerio.Of[Config](42)
// result := readerio.MonadChainFirstIOK(rio, func(n int) io.IO[string] {
// return io.Of(fmt.Sprintf("Logged: %d", n))
// })
// value := result(config)() // Returns 42
func MonadChainFirstIOK[R, A, B any](ma ReaderIO[R, A], f io.Kleisli[A, B]) ReaderIO[R, A] {
return fromio.MonadChainFirstIOK(
MonadChain[R, A, A],
MonadMap[R, B, A],
FromIO[R, B],
ma, f,
)
}
// MonadTapIOK chains a ReaderIO with an IO-returning function but keeps the original value.
// This is an alias for [MonadChainFirstIOK] and is useful for side effects like logging.
//
// Type Parameters:
// - R: Reader environment type
// - A: Input and output value type
// - B: IO result type (discarded)
//
// Parameters:
// - ma: The ReaderIO to tap
// - f: Function that takes a value and returns an IO for side effects
//
// Returns:
// - A ReaderIO with the original value after executing the IO
//
// Example:
//
// result := readerio.MonadTapIOK(
// readerio.Of[Config](42),
// func(n int) io.IO[func()] {
// return io.Of(func() { fmt.Println(n) })
// },
// )
func MonadTapIOK[R, A, B any](ma ReaderIO[R, A], f io.Kleisli[A, B]) ReaderIO[R, A] {
return MonadChainFirstIOK(ma, f)
}
// ChainIOK creates a function that chains a ReaderIO with an IO operation.
// This is the curried version suitable for use in pipelines.
//
@@ -443,7 +667,7 @@ func MonadChainIOK[R, A, B any](ma ReaderIO[R, A], f func(A) IO[B]) ReaderIO[R,
// return io.Of(n * 2)
// }),
// )(config)() // Returns 10
func ChainIOK[R, A, B any](f func(A) IO[B]) Operator[R, A, B] {
func ChainIOK[R, A, B any](f io.Kleisli[A, B]) Operator[R, A, B] {
return fromio.ChainIOK(
Chain[R, A, B],
FromIO[R, B],
@@ -451,6 +675,63 @@ func ChainIOK[R, A, B any](f func(A) IO[B]) Operator[R, A, B] {
)
}
// ChainFirstIOK creates a function that chains a ReaderIO with an IO operation but keeps the original value.
// This is the curried version of [MonadChainFirstIOK], suitable for use in pipelines.
//
// Type Parameters:
// - R: Reader environment type
// - A: Input and output value type
// - B: IO result type (discarded)
//
// Parameters:
// - f: Function that takes a value and returns an IO
//
// Returns:
// - An Operator that chains with IO while preserving the original value
//
// Example:
//
// result := F.Pipe1(
// readerio.Of[Config](42),
// readerio.ChainFirstIOK(func(n int) io.IO[string] {
// return io.Of(fmt.Sprintf("Logged: %d", n))
// }),
// )(config)() // Returns 42
func ChainFirstIOK[R, A, B any](f io.Kleisli[A, B]) Operator[R, A, A] {
return fromio.ChainFirstIOK(
Chain[R, A, A],
Map[R, B, A],
FromIO[R, B],
f,
)
}
// TapIOK creates a function that chains a ReaderIO with an IO operation but keeps the original value.
// This is the curried version of [MonadTapIOK], an alias for [ChainFirstIOK].
//
// Type Parameters:
// - R: Reader environment type
// - A: Input and output value type
// - B: IO result type (discarded)
//
// Parameters:
// - f: Function that takes a value and returns an IO for side effects
//
// Returns:
// - An Operator that taps with IO-returning functions
//
// Example:
//
// result := F.Pipe1(
// readerio.Of[Config](42),
// readerio.TapIOK(func(n int) io.IO[func()] {
// return io.Of(func() { fmt.Println(n) })
// }),
// )(config)() // Returns 42, prints 42
func TapIOK[R, A, B any](f io.Kleisli[A, B]) Operator[R, A, A] {
return ChainFirstIOK[R](f)
}
// Defer creates a ReaderIO by calling a generator function each time it's executed.
// This allows for lazy evaluation and ensures a fresh computation on each invocation.
// Useful for operations that should not be cached or memoized.
@@ -565,9 +846,9 @@ func Flatten[R, A any](mma ReaderIO[R, ReaderIO[R, A]]) ReaderIO[R, A] {
//
// Example:
//
// fabIO := readerio.Of[Config](func(n int) int { return n * 2 })
// fabIO := readerio.Of[Config](N.Mul(2))
// result := readerio.MonadFlap(fabIO, 5)(config)() // Returns 10
func MonadFlap[R, A, B any](fab ReaderIO[R, func(A) B], a A) ReaderIO[R, B] {
func MonadFlap[R, B, A any](fab ReaderIO[R, func(A) B], a A) ReaderIO[R, B] {
return functor.MonadFlap(MonadMap[R, func(A) B, B], fab, a)
}
@@ -588,9 +869,224 @@ func MonadFlap[R, A, B any](fab ReaderIO[R, func(A) B], a A) ReaderIO[R, B] {
// Example:
//
// result := F.Pipe1(
// readerio.Of[Config](func(n int) int { return n * 2 }),
// readerio.Of[Config](N.Mul(2)),
// readerio.Flap[Config](5),
// )(config)() // Returns 10
func Flap[R, A, B any](a A) Operator[R, func(A) B, B] {
//
//go:inline
func Flap[R, B, A any](a A) Operator[R, func(A) B, B] {
return functor.Flap(Map[R, func(A) B, 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.
//
// Type Parameters:
// - R: Reader environment type
// - A: Input value type
// - B: Output value type
//
// Parameters:
// - ma: The ReaderIO to chain from
// - f: Function that produces a Reader
//
// Returns:
// - A new ReaderIO with the chained Reader computation
//
// Example:
//
// rio := readerio.Of[Config](5)
// result := readerio.MonadChainReaderK(rio, func(n int) reader.Reader[Config, int] {
// return func(c Config) int { return n + c.Value }
// })
//
//go:inline
func MonadChainReaderK[R, A, B any](ma ReaderIO[R, A], f reader.Kleisli[R, A, B]) ReaderIO[R, B] {
return fromreader.MonadChainReaderK(
MonadChain,
FromReader,
ma,
f,
)
}
// ChainReaderK creates a function that chains a ReaderIO with a Reader-returning function.
// This is the curried version of [MonadChainReaderK], suitable for use in pipelines.
//
// Type Parameters:
// - R: Reader environment type
// - A: Input value type
// - B: Output value type
//
// Parameters:
// - f: Function that produces a Reader
//
// Returns:
// - An Operator that chains Reader-returning functions
//
// Example:
//
// result := F.Pipe1(
// readerio.Of[Config](5),
// readerio.ChainReaderK(func(n int) reader.Reader[Config, int] {
// return func(c Config) int { return n + c.Value }
// }),
// )(config)()
//
//go:inline
func ChainReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, B] {
return fromreader.ChainReaderK(
Chain,
FromReader,
f,
)
}
// MonadChainFirstReaderK chains a function that returns a Reader but keeps the original value.
// The Reader computation is executed for its side effects only.
//
// Type Parameters:
// - R: Reader environment type
// - A: Input and output value type
// - B: Reader result type (discarded)
//
// Parameters:
// - ma: The ReaderIO to chain from
// - f: Function that produces a Reader
//
// Returns:
// - A ReaderIO with the original value after executing the Reader
//
// Example:
//
// rio := readerio.Of[Config](42)
// result := readerio.MonadChainFirstReaderK(rio, func(n int) reader.Reader[Config, string] {
// return func(c Config) string { return fmt.Sprintf("Logged: %d", n) }
// })
// value := result(config)() // Returns 42
//
//go:inline
func MonadChainFirstReaderK[R, A, B any](ma ReaderIO[R, A], f reader.Kleisli[R, A, B]) ReaderIO[R, A] {
return fromreader.MonadChainFirstReaderK(
MonadChainFirst[R, A, B],
FromReader[R, B],
ma,
f,
)
}
// ChainFirstReaderK creates a function that chains a Reader but keeps the original value.
// This is the curried version of [MonadChainFirstReaderK], suitable for use in pipelines.
//
// Type Parameters:
// - R: Reader environment type
// - A: Input and output value type
// - B: Reader result type (discarded)
//
// Parameters:
// - f: Function that produces a Reader
//
// Returns:
// - An Operator that chains Reader-returning functions while preserving the original value
//
// Example:
//
// result := F.Pipe1(
// readerio.Of[Config](42),
// readerio.ChainFirstReaderK(func(n int) reader.Reader[Config, string] {
// return func(c Config) string { return fmt.Sprintf("Logged: %d", n) }
// }),
// )(config)() // Returns 42
//
//go:inline
func ChainFirstReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, A] {
return fromreader.ChainFirstReaderK(
ChainFirst[R, A, B],
FromReader[R, B],
f,
)
}
// 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.
//
// Type Parameters:
// - R: Reader environment type
// - A: Input and output value type
// - B: Reader result type (discarded)
//
// 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
//
// Example:
//
// result := readerio.MonadTapReaderK(
// readerio.Of[Config](42),
// func(n int) reader.Reader[Config, func()] {
// return func(c Config) func() { return func() { fmt.Println(n) } }
// },
// )
//
//go:inline
func MonadTapReaderK[R, A, B any](ma ReaderIO[R, A], f reader.Kleisli[R, A, B]) ReaderIO[R, A] {
return MonadChainFirstReaderK(ma, f)
}
// TapReaderK creates a function that chains a Reader but keeps the original value.
// This is the curried version of [MonadTapReaderK], an alias for [ChainFirstReaderK].
//
// Type Parameters:
// - R: Reader environment type
// - A: Input and output value type
// - B: Reader result type (discarded)
//
// Parameters:
// - f: Function that produces a Reader for side effects
//
// Returns:
// - An Operator that taps with Reader-returning functions
//
// Example:
//
// result := F.Pipe1(
// readerio.Of[Config](42),
// readerio.TapReaderK(func(n int) reader.Reader[Config, func()] {
// return func(c Config) func() { return func() { fmt.Println(n) } }
// }),
// )(config)() // Returns 42, prints 42
//
//go:inline
func TapReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, A] {
return ChainFirstReaderK(f)
}
// Read executes a ReaderIO with a given environment, returning the resulting IO.
// This is useful for providing the environment dependency and obtaining an IO action
// that can be executed later.
//
// Type Parameters:
// - A: Result type
// - R: Reader environment type
//
// Parameters:
// - r: The environment to provide to the ReaderIO
//
// Returns:
// - A function that converts a ReaderIO into an IO by applying the environment
//
// Example:
//
// rio := readerio.Of[Config](42)
// config := Config{Value: 10, Name: "test"}
// ioAction := readerio.Read[int](config)(rio)
// result := ioAction() // Returns 42
//
//go:inline
func Read[A, R any](r R) func(ReaderIO[R, A]) IO[A] {
return reader.Read[IO[A]](r)
}

View File

@@ -22,6 +22,7 @@ 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"
)
@@ -62,7 +63,7 @@ func TestOf(t *testing.T) {
func TestMonadMap(t *testing.T) {
rio := Of[ReaderTestConfig](5)
doubled := MonadMap(rio, func(n int) int { return n * 2 })
doubled := MonadMap(rio, N.Mul(2))
config := ReaderTestConfig{Value: 1, Name: "test"}
result := doubled(config)()
@@ -102,7 +103,7 @@ func TestChain(t *testing.T) {
}
func TestMonadAp(t *testing.T) {
fabIO := Of[ReaderTestConfig](func(n int) int { return n * 2 })
fabIO := Of[ReaderTestConfig](N.Mul(2))
faIO := Of[ReaderTestConfig](5)
result := MonadAp(fabIO, faIO)
@@ -120,7 +121,7 @@ func TestAp(t *testing.T) {
}
func TestMonadApSeq(t *testing.T) {
fabIO := Of[ReaderTestConfig](func(n int) int { return n + 10 })
fabIO := Of[ReaderTestConfig](N.Add(10))
faIO := Of[ReaderTestConfig](5)
result := MonadApSeq(fabIO, faIO)
@@ -129,7 +130,7 @@ func TestMonadApSeq(t *testing.T) {
}
func TestMonadApPar(t *testing.T) {
fabIO := Of[ReaderTestConfig](func(n int) int { return n + 10 })
fabIO := Of[ReaderTestConfig](N.Add(10))
faIO := Of[ReaderTestConfig](5)
result := MonadApPar(fabIO, faIO)
@@ -238,7 +239,7 @@ func TestFlatten(t *testing.T) {
}
func TestMonadFlap(t *testing.T) {
fabIO := Of[ReaderTestConfig](func(n int) int { return n * 3 })
fabIO := Of[ReaderTestConfig](N.Mul(3))
result := MonadFlap(fabIO, 7)
config := ReaderTestConfig{Value: 1, Name: "test"}
@@ -247,7 +248,7 @@ func TestMonadFlap(t *testing.T) {
func TestFlap(t *testing.T) {
result := F.Pipe1(
Of[ReaderTestConfig](func(n int) int { return n * 3 }),
Of[ReaderTestConfig](N.Mul(3)),
Flap[ReaderTestConfig, int, int](7),
)
@@ -263,7 +264,7 @@ func TestComplexPipeline(t *testing.T) {
Chain(func(n int) ReaderIO[ReaderTestConfig, int] {
return Of[ReaderTestConfig](n * 2)
}),
Map[ReaderTestConfig](func(n int) int { return n + 10 }),
Map[ReaderTestConfig](N.Add(10)),
)
config := ReaderTestConfig{Value: 5, Name: "test"}
@@ -302,4 +303,264 @@ func TestFromReaderWithMap(t *testing.T) {
assert.Equal(t, "original modified", result(config)())
}
// Made with Bob
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

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

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

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