mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-19 23:42:05 +02:00
fix: try to unify type signatures
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
This commit is contained in:
574
v2/DESIGN.md
Normal file
574
v2/DESIGN.md
Normal file
@@ -0,0 +1,574 @@
|
||||
# Design Decisions
|
||||
|
||||
This document explains the key design decisions and principles behind fp-go's API design.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Data Last Principle](#data-last-principle)
|
||||
- [Kleisli and Operator Types](#kleisli-and-operator-types)
|
||||
- [Monadic Operations Comparison](#monadic-operations-comparison)
|
||||
- [Type Parameter Ordering](#type-parameter-ordering)
|
||||
- [Generic Type Aliases](#generic-type-aliases)
|
||||
|
||||
## Data Last Principle
|
||||
|
||||
fp-go follows the **"data last"** principle, where the data being operated on is always the last parameter in a function. This design choice enables powerful function composition and partial application patterns.
|
||||
|
||||
### What is "Data Last"?
|
||||
|
||||
In the "data last" style, functions are structured so that:
|
||||
1. Configuration parameters come first
|
||||
2. The data to be transformed comes last
|
||||
|
||||
This is the opposite of the traditional object-oriented style where the data (receiver) comes first.
|
||||
|
||||
### Why "Data Last"?
|
||||
|
||||
The "data last" principle enables:
|
||||
|
||||
1. **Natural Currying**: Functions can be partially applied to create specialized transformations
|
||||
2. **Function Composition**: Operations can be composed before applying them to data
|
||||
3. **Point-Free Style**: Write transformations without explicitly mentioning the data
|
||||
4. **Reusability**: Create reusable transformation pipelines
|
||||
|
||||
### Examples
|
||||
|
||||
#### Basic Transformation
|
||||
|
||||
```go
|
||||
// Data last style (fp-go)
|
||||
double := array.Map(number.Mul(2))
|
||||
result := double([]int{1, 2, 3}) // [2, 4, 6]
|
||||
|
||||
// Compare with data first style (traditional)
|
||||
result := array.Map([]int{1, 2, 3}, number.Mul(2))
|
||||
```
|
||||
|
||||
#### Function Composition
|
||||
|
||||
```go
|
||||
import (
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
// Create a pipeline of transformations
|
||||
pipeline := F.Flow3(
|
||||
A.Filter(func(x int) bool { return x > 0 }), // Keep positive numbers
|
||||
A.Map(N.Mul(2)), // Double each number
|
||||
A.Reduce(func(acc, x int) int { return acc + x }, 0), // Sum them up
|
||||
)
|
||||
|
||||
// Apply the pipeline to different data
|
||||
result1 := pipeline([]int{-1, 2, 3, -4, 5}) // (2 + 3 + 5) * 2 = 20
|
||||
result2 := pipeline([]int{1, 2, 3}) // (1 + 2 + 3) * 2 = 12
|
||||
```
|
||||
|
||||
#### Partial Application
|
||||
|
||||
```go
|
||||
import (
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// Create specialized functions by partial application
|
||||
getOrZero := O.GetOrElse(func() int { return 0 })
|
||||
getOrEmpty := O.GetOrElse(func() string { return "" })
|
||||
|
||||
// Use them with different data
|
||||
value1 := getOrZero(O.Some(42)) // 42
|
||||
value2 := getOrZero(O.None[int]()) // 0
|
||||
|
||||
text1 := getOrEmpty(O.Some("hello")) // "hello"
|
||||
text2 := getOrEmpty(O.None[string]()) // ""
|
||||
```
|
||||
|
||||
#### Building Reusable Transformations
|
||||
|
||||
```go
|
||||
import (
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// Create a reusable validation pipeline
|
||||
type User struct {
|
||||
Name string
|
||||
Email string
|
||||
Age int
|
||||
}
|
||||
|
||||
validateAge := E.FromPredicate(
|
||||
func(u User) bool { return u.Age >= 18 },
|
||||
func(u User) error { return errors.New("must be 18 or older") },
|
||||
)
|
||||
|
||||
validateEmail := E.FromPredicate(
|
||||
func(u User) bool { return strings.Contains(u.Email, "@") },
|
||||
func(u User) error { return errors.New("invalid email") },
|
||||
)
|
||||
|
||||
// Compose validators
|
||||
validateUser := F.Flow2(
|
||||
validateAge,
|
||||
E.Chain(validateEmail),
|
||||
)
|
||||
|
||||
// Apply to different users
|
||||
result1 := validateUser(User{Name: "Alice", Email: "alice@example.com", Age: 25})
|
||||
result2 := validateUser(User{Name: "Bob", Email: "invalid", Age: 30})
|
||||
```
|
||||
|
||||
#### Monadic Operations
|
||||
|
||||
```go
|
||||
import (
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// Data last enables clean monadic chains
|
||||
parseAndDouble := F.Flow2(
|
||||
O.FromPredicate(func(s string) bool { return s != "" }),
|
||||
O.Chain(func(s string) O.Option[int] {
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return O.None[int]()
|
||||
}
|
||||
return O.Some(n * 2)
|
||||
}),
|
||||
)
|
||||
|
||||
result1 := parseAndDouble("21") // Some(42)
|
||||
result2 := parseAndDouble("") // None
|
||||
result3 := parseAndDouble("abc") // None
|
||||
```
|
||||
|
||||
### Monadic vs Non-Monadic Forms
|
||||
|
||||
fp-go provides two forms for most operations:
|
||||
|
||||
1. **Curried form** (data last): Returns a function that can be composed
|
||||
2. **Monadic form** (data first): Takes all parameters at once
|
||||
|
||||
```go
|
||||
// Curried form - data last, returns a function
|
||||
Map[A, B any](f func(A) B) func(Option[A]) Option[B]
|
||||
|
||||
// Monadic form - data first, direct execution
|
||||
MonadMap[A, B any](fa Option[A], f func(A) B) Option[B]
|
||||
```
|
||||
|
||||
**When to use each:**
|
||||
|
||||
- **Curried form**: When building pipelines, composing functions, or creating reusable transformations
|
||||
- **Monadic form**: When you have all parameters available and want direct execution
|
||||
|
||||
```go
|
||||
// Curried form - building a pipeline
|
||||
transform := F.Flow3(
|
||||
O.Map(strings.ToUpper),
|
||||
O.Filter(func(s string) bool { return len(s) > 3 }),
|
||||
O.GetOrElse(func() string { return "DEFAULT" }),
|
||||
)
|
||||
result := transform(O.Some("hello"))
|
||||
|
||||
// Monadic form - direct execution
|
||||
result := O.MonadMap(O.Some("hello"), strings.ToUpper)
|
||||
```
|
||||
|
||||
### Further Reading on Data-Last Pattern
|
||||
|
||||
The data-last currying pattern is well-documented in the functional programming community:
|
||||
|
||||
- [Mostly Adequate Guide - Ch. 4: Currying](https://mostly-adequate.gitbook.io/mostly-adequate-guide/ch04) - Excellent introduction with clear examples
|
||||
- [Curry and Function Composition](https://medium.com/javascript-scene/curry-and-function-composition-2c208d774983) by Eric Elliott
|
||||
- [fp-ts Issue #1238](https://github.com/gcanti/fp-ts/issues/1238) - Real-world examples of data-last refactoring
|
||||
|
||||
## Kleisli and Operator Types
|
||||
|
||||
fp-go uses consistent type aliases across all monads to make code more recognizable and composable. These types provide a common vocabulary that works across different monadic contexts.
|
||||
|
||||
### Type Definitions
|
||||
|
||||
```go
|
||||
// Kleisli arrow - a function that returns a monadic value
|
||||
type Kleisli[A, B any] = func(A) M[B]
|
||||
|
||||
// Operator - a function that transforms a monadic value
|
||||
type Operator[A, B any] = func(M[A]) M[B]
|
||||
```
|
||||
|
||||
Where `M` represents the specific monad (Option, Either, IO, etc.).
|
||||
|
||||
### Why These Types Matter
|
||||
|
||||
1. **Consistency**: The same type names appear across all monads
|
||||
2. **Recognizability**: Experienced functional programmers immediately understand the intent
|
||||
3. **Composability**: Functions with these types compose naturally
|
||||
4. **Documentation**: Type signatures clearly communicate the operation's behavior
|
||||
|
||||
### Examples Across Monads
|
||||
|
||||
#### Option Monad
|
||||
|
||||
```go
|
||||
// option/option.go
|
||||
type Kleisli[A, B any] = func(A) Option[B]
|
||||
type Operator[A, B any] = func(Option[A]) Option[B]
|
||||
|
||||
// Chain uses Kleisli
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B]
|
||||
|
||||
// Map returns an Operator
|
||||
func Map[A, B any](f func(A) B) Operator[A, B]
|
||||
```
|
||||
|
||||
#### Either Monad
|
||||
|
||||
```go
|
||||
// either/either.go
|
||||
type Kleisli[E, A, B any] = func(A) Either[E, B]
|
||||
type Operator[E, A, B any] = func(Either[E, A]) Either[E, B]
|
||||
|
||||
// Chain uses Kleisli
|
||||
func Chain[E, A, B any](f Kleisli[E, A, B]) Operator[E, A, B]
|
||||
|
||||
// Map returns an Operator
|
||||
func Map[E, A, B any](f func(A) B) Operator[E, A, B]
|
||||
```
|
||||
|
||||
#### IO Monad
|
||||
|
||||
```go
|
||||
// io/io.go
|
||||
type Kleisli[A, B any] = func(A) IO[B]
|
||||
type Operator[A, B any] = func(IO[A]) IO[B]
|
||||
|
||||
// Chain uses Kleisli
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B]
|
||||
|
||||
// Map returns an Operator
|
||||
func Map[A, B any](f func(A) B) Operator[A, B]
|
||||
```
|
||||
|
||||
#### Array (List Monad)
|
||||
|
||||
```go
|
||||
// array/array.go
|
||||
type Kleisli[A, B any] = func(A) []B
|
||||
type Operator[A, B any] = func([]A) []B
|
||||
|
||||
// Chain uses Kleisli
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B]
|
||||
|
||||
// Map returns an Operator
|
||||
func Map[A, B any](f func(A) B) Operator[A, B]
|
||||
```
|
||||
|
||||
### Pattern Recognition
|
||||
|
||||
Once you learn these patterns in one monad, you can apply them to all monads:
|
||||
|
||||
```go
|
||||
// The pattern is always the same, just the monad changes
|
||||
|
||||
// Option
|
||||
validateAge := option.Chain(func(user User) option.Option[User] {
|
||||
if user.Age >= 18 {
|
||||
return option.Some(user)
|
||||
}
|
||||
return option.None[User]()
|
||||
})
|
||||
|
||||
// Either
|
||||
validateAge := either.Chain(func(user User) either.Either[error, User] {
|
||||
if user.Age >= 18 {
|
||||
return either.Right[error](user)
|
||||
}
|
||||
return either.Left[User](errors.New("too young"))
|
||||
})
|
||||
|
||||
// IO
|
||||
validateAge := io.Chain(func(user User) io.IO[User] {
|
||||
return io.Of(user) // Always succeeds in IO
|
||||
})
|
||||
|
||||
// Array
|
||||
validateAge := array.Chain(func(user User) []User {
|
||||
if user.Age >= 18 {
|
||||
return []User{user}
|
||||
}
|
||||
return []User{} // Empty array = failure
|
||||
})
|
||||
```
|
||||
|
||||
### Composing Kleisli Arrows
|
||||
|
||||
Kleisli arrows compose naturally using monadic composition:
|
||||
|
||||
```go
|
||||
import (
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
// Define Kleisli arrows
|
||||
parseAge := func(s string) O.Option[int] {
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return O.None[int]()
|
||||
}
|
||||
return O.Some(n)
|
||||
}
|
||||
|
||||
validateAge := func(age int) O.Option[int] {
|
||||
if age >= 18 {
|
||||
return O.Some(age)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
|
||||
formatAge := func(age int) O.Option[string] {
|
||||
return O.Some(fmt.Sprintf("Age: %d", age))
|
||||
}
|
||||
|
||||
// Compose them using Flow and Chain
|
||||
pipeline := F.Flow3(
|
||||
parseAge,
|
||||
O.Chain(validateAge),
|
||||
O.Chain(formatAge),
|
||||
)
|
||||
|
||||
result := pipeline("25") // Some("Age: 25")
|
||||
result := pipeline("15") // None (too young)
|
||||
result := pipeline("abc") // None (parse error)
|
||||
```
|
||||
|
||||
### Building Reusable Operators
|
||||
|
||||
Operators can be created once and reused across your codebase:
|
||||
|
||||
```go
|
||||
import (
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
)
|
||||
|
||||
// Create reusable operators
|
||||
type ValidationError struct {
|
||||
Field string
|
||||
Message string
|
||||
}
|
||||
|
||||
// Reusable validation operators
|
||||
validateNonEmpty := E.Chain(func(s string) E.Either[ValidationError, string] {
|
||||
if s == "" {
|
||||
return E.Left[string](ValidationError{
|
||||
Field: "input",
|
||||
Message: "cannot be empty",
|
||||
})
|
||||
}
|
||||
return E.Right[ValidationError](s)
|
||||
})
|
||||
|
||||
validateEmail := E.Chain(func(s string) E.Either[ValidationError, string] {
|
||||
if !strings.Contains(s, "@") {
|
||||
return E.Left[string](ValidationError{
|
||||
Field: "email",
|
||||
Message: "invalid format",
|
||||
})
|
||||
}
|
||||
return E.Right[ValidationError](s)
|
||||
})
|
||||
|
||||
// Compose operators
|
||||
validateEmailInput := F.Flow2(
|
||||
validateNonEmpty,
|
||||
validateEmail,
|
||||
)
|
||||
|
||||
// Use across your application
|
||||
result1 := validateEmailInput(E.Right[ValidationError]("user@example.com"))
|
||||
result2 := validateEmailInput(E.Right[ValidationError](""))
|
||||
result3 := validateEmailInput(E.Right[ValidationError]("invalid"))
|
||||
```
|
||||
|
||||
### Benefits of Consistent Naming
|
||||
|
||||
1. **Cross-monad understanding**: Learn once, apply everywhere
|
||||
2. **Easier refactoring**: Changing monads requires minimal code changes
|
||||
3. **Better tooling**: IDEs can provide better suggestions
|
||||
4. **Team communication**: Shared vocabulary across the team
|
||||
5. **Library integration**: Third-party libraries follow the same patterns
|
||||
|
||||
### Identity Monad - The Simplest Case
|
||||
|
||||
The Identity monad shows these types in their simplest form:
|
||||
|
||||
```go
|
||||
// identity/doc.go
|
||||
type Operator[A, B any] = func(A) B
|
||||
|
||||
// In Identity, there's no wrapping, so:
|
||||
// - Kleisli[A, B] is just func(A) B
|
||||
// - Operator[A, B] is just func(A) B
|
||||
// They're the same because Identity adds no context
|
||||
```
|
||||
|
||||
This demonstrates that these type aliases represent fundamental functional programming concepts, not just arbitrary naming conventions.
|
||||
|
||||
|
||||
## Monadic Operations Comparison
|
||||
|
||||
fp-go's monadic operations are inspired by functional programming languages and libraries. Here's how they compare:
|
||||
|
||||
| fp-go | fp-ts | Haskell | Scala | Description |
|
||||
|-------|-------|---------|-------|-------------|
|
||||
| `Map` | `map` | `fmap` | `map` | Functor mapping - transforms the value inside a context |
|
||||
| `Chain` | `chain` | `>>=` (bind) | `flatMap` | Monadic bind - chains computations that return wrapped values |
|
||||
| `Ap` | `ap` | `<*>` | `ap` | Applicative apply - applies a wrapped function to a wrapped value |
|
||||
| `Of` | `of` | `return`/`pure` | `pure` | Lifts a pure value into a monadic context |
|
||||
| `Fold` | `fold` | `either` | `fold` | Eliminates the context by providing handlers for each case |
|
||||
| `Filter` | `filter` | `mfilter` | `filter` | Keeps values that satisfy a predicate |
|
||||
| `Flatten` | `flatten` | `join` | `flatten` | Removes one level of nesting |
|
||||
| `ChainFirst` | `chainFirst` | `>>` (then) | `tap` | Chains for side effects, keeping the original value |
|
||||
| `Alt` | `alt` | `<\|>` | `orElse` | Provides an alternative value if the first fails |
|
||||
| `GetOrElse` | `getOrElse` | `fromMaybe` | `getOrElse` | Extracts the value or provides a default |
|
||||
| `FromPredicate` | `fromPredicate` | `guard` | `filter` | Creates a monadic value based on a predicate |
|
||||
| `Sequence` | `sequence` | `sequence` | `sequence` | Transforms a collection of effects into an effect of a collection |
|
||||
| `Traverse` | `traverse` | `traverse` | `traverse` | Maps and sequences in one operation |
|
||||
| `Reduce` | `reduce` | `foldl` | `foldLeft` | Folds a structure from left to right |
|
||||
| `ReduceRight` | `reduceRight` | `foldr` | `foldRight` | Folds a structure from right to left |
|
||||
|
||||
### Key Differences from Other Languages
|
||||
|
||||
#### Naming Conventions
|
||||
|
||||
- **Go conventions**: fp-go uses PascalCase for exported functions (e.g., `Map`, `Chain`) following Go's naming conventions
|
||||
- **Type parameters first**: Non-inferrable type parameters come first (e.g., `Ap[B, E, A any]`)
|
||||
- **Monadic prefix**: Direct execution forms use the `Monad` prefix (e.g., `MonadMap`, `MonadChain`)
|
||||
|
||||
#### Type System
|
||||
|
||||
```go
|
||||
// fp-go (explicit type parameters when needed)
|
||||
result := option.Map(transform)(value)
|
||||
result := option.Map[string, int](transform)(value) // explicit when inference fails
|
||||
|
||||
// Haskell (type inference)
|
||||
result = fmap transform value
|
||||
|
||||
// Scala (type inference with method syntax)
|
||||
result = value.map(transform)
|
||||
|
||||
// fp-ts (TypeScript type inference)
|
||||
const result = pipe(value, map(transform))
|
||||
```
|
||||
|
||||
#### Currying
|
||||
|
||||
```go
|
||||
// fp-go - explicit currying with data last
|
||||
double := array.Map(number.Mul(2))
|
||||
result := double(numbers)
|
||||
|
||||
// Haskell - automatic currying
|
||||
double = fmap (*2)
|
||||
result = double numbers
|
||||
|
||||
// Scala - method syntax
|
||||
result = numbers.map(_ * 2)
|
||||
```
|
||||
|
||||
## Type Parameter Ordering
|
||||
|
||||
fp-go v2 uses a specific ordering for type parameters to maximize type inference:
|
||||
|
||||
### Rule: Non-Inferrable Parameters First
|
||||
|
||||
Type parameters that **cannot be inferred** from function arguments come first. This allows the Go compiler to infer as many types as possible.
|
||||
|
||||
```go
|
||||
// Ap - B cannot be inferred from arguments, so it comes first
|
||||
func Ap[B, E, A any](fa Either[E, A]) func(Either[E, func(A) B]) Either[E, B]
|
||||
|
||||
// Usage - only B needs to be specified
|
||||
result := either.Ap[string](value)(funcInEither)
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```go
|
||||
// Map - all types can be inferred from arguments
|
||||
func Map[E, A, B any](f func(A) B) func(Either[E, A]) Either[E, B]
|
||||
// Usage - no type parameters needed
|
||||
result := either.Map(transform)(value)
|
||||
|
||||
// Chain - all types can be inferred
|
||||
func Chain[E, A, B any](f func(A) Either[E, B]) func(Either[E, A]) Either[E, B]
|
||||
// Usage - no type parameters needed
|
||||
result := either.Chain(validator)(value)
|
||||
|
||||
// Of - E cannot be inferred, comes first
|
||||
func Of[E, A any](value A) Either[E, A]
|
||||
// Usage - only E needs to be specified
|
||||
result := either.Of[error](42)
|
||||
```
|
||||
|
||||
### Benefits
|
||||
|
||||
1. **Less verbose code**: Most operations don't require explicit type parameters
|
||||
2. **Better IDE support**: Type inference provides better autocomplete
|
||||
3. **Clearer intent**: Only specify types that can't be inferred
|
||||
|
||||
## Generic Type Aliases
|
||||
|
||||
fp-go v2 leverages Go 1.24's generic type aliases for cleaner type definitions:
|
||||
|
||||
```go
|
||||
// V2 - using generic type alias (requires Go 1.24+)
|
||||
type ReaderIOEither[R, E, A any] = RD.Reader[R, IOE.IOEither[E, A]]
|
||||
|
||||
// V1 - using type definition (Go 1.18+)
|
||||
type ReaderIOEither[R, E, A any] RD.Reader[R, IOE.IOEither[E, A]]
|
||||
```
|
||||
|
||||
### Benefits
|
||||
|
||||
1. **True aliases**: The type is interchangeable with its definition
|
||||
2. **No namespace imports needed**: Can use types directly without package prefixes
|
||||
3. **Simpler codebase**: Eliminates the need for `generic` subpackages
|
||||
4. **Better composability**: Types compose more naturally
|
||||
|
||||
### Migration Pattern
|
||||
|
||||
```go
|
||||
// Define project-wide aliases once
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
)
|
||||
|
||||
type Option[A any] = option.Option[A]
|
||||
type Result[A any] = result.Result[A]
|
||||
type IOResult[A any] = ioresult.IOResult[A]
|
||||
|
||||
// Use throughout your codebase
|
||||
package myapp
|
||||
|
||||
import "myproject/types"
|
||||
|
||||
func process(input string) types.Result[types.Option[int]] {
|
||||
// implementation
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
For more information, see:
|
||||
- [README.md](./README.md) - Overview and quick start
|
||||
- [API Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2) - Complete API reference
|
||||
- [Samples](./samples/) - Practical examples
|
||||
@@ -19,6 +19,58 @@
|
||||
// allowing for composable and functional test assertions. Each assertion
|
||||
// returns a Reader that takes a *testing.T and performs the assertion.
|
||||
//
|
||||
// # Data Last Principle
|
||||
//
|
||||
// This package follows the "data last" functional programming principle, where
|
||||
// the data being operated on comes as the last parameter in a chain of function
|
||||
// applications. This design enables several powerful functional programming patterns:
|
||||
//
|
||||
// 1. **Partial Application**: You can create reusable assertion functions by providing
|
||||
// configuration parameters first, leaving the data and testing context for later.
|
||||
//
|
||||
// 2. **Function Composition**: Assertions can be composed and combined before being
|
||||
// applied to actual data.
|
||||
//
|
||||
// 3. **Point-Free Style**: You can pass assertion functions around without immediately
|
||||
// providing the data they operate on.
|
||||
//
|
||||
// The general pattern is:
|
||||
//
|
||||
// assert.Function(config)(data)(testingContext)
|
||||
// ↑ ↑ ↑
|
||||
// expected actual *testing.T (always last)
|
||||
//
|
||||
// For single-parameter assertions:
|
||||
//
|
||||
// assert.Function(data)(testingContext)
|
||||
// ↑ ↑
|
||||
// actual *testing.T (always last)
|
||||
//
|
||||
// Examples of "data last" in action:
|
||||
//
|
||||
// // Multi-parameter: expected value → actual value → testing context
|
||||
// assert.Equal(42)(result)(t)
|
||||
// assert.ArrayContains(3)(numbers)(t)
|
||||
//
|
||||
// // Single-parameter: data → testing context
|
||||
// assert.NoError(err)(t)
|
||||
// assert.ArrayNotEmpty(arr)(t)
|
||||
//
|
||||
// // Partial application - create reusable assertions
|
||||
// isPositive := assert.That(func(n int) bool { return n > 0 })
|
||||
// // Later, apply to different values:
|
||||
// isPositive(42)(t) // Passes
|
||||
// isPositive(-5)(t) // Fails
|
||||
//
|
||||
// // Composition - combine assertions before applying data
|
||||
// validateUser := func(u User) assert.Reader {
|
||||
// return assert.AllOf([]assert.Reader{
|
||||
// assert.Equal("Alice")(u.Name),
|
||||
// assert.That(func(age int) bool { return age >= 18 })(u.Age),
|
||||
// })
|
||||
// }
|
||||
// validateUser(user)(t)
|
||||
//
|
||||
// The package supports:
|
||||
// - Equality and inequality assertions
|
||||
// - Collection assertions (arrays, maps, strings)
|
||||
@@ -83,38 +135,108 @@ func wrap1[T any](wrapped func(t assert.TestingT, expected, actual any, msgAndAr
|
||||
}
|
||||
}
|
||||
|
||||
// NotEqual tests if the expected and the actual values are not equal
|
||||
// NotEqual tests if the expected and the actual values are not equal.
|
||||
//
|
||||
// This function follows the "data last" principle - you provide the expected value first,
|
||||
// then the actual value, and finally the testing.T context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestNotEqual(t *testing.T) {
|
||||
// value := 42
|
||||
// assert.NotEqual(10)(value)(t) // Passes: 42 != 10
|
||||
// assert.NotEqual(42)(value)(t) // Fails: 42 == 42
|
||||
// }
|
||||
func NotEqual[T any](expected T) Kleisli[T] {
|
||||
return wrap1(assert.NotEqual, expected)
|
||||
}
|
||||
|
||||
// Equal tests if the expected and the actual values are equal
|
||||
// Equal tests if the expected and the actual values are equal.
|
||||
//
|
||||
// This is one of the most commonly used assertions. It follows the "data last" principle -
|
||||
// you provide the expected value first, then the actual value, and finally the testing.T context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestEqual(t *testing.T) {
|
||||
// result := 2 + 2
|
||||
// assert.Equal(4)(result)(t) // Passes
|
||||
//
|
||||
// name := "Alice"
|
||||
// assert.Equal("Alice")(name)(t) // Passes
|
||||
//
|
||||
// // Can be composed with other assertions
|
||||
// user := User{Name: "Bob", Age: 30}
|
||||
// assertions := assert.AllOf([]assert.Reader{
|
||||
// assert.Equal("Bob")(user.Name),
|
||||
// assert.Equal(30)(user.Age),
|
||||
// })
|
||||
// assertions(t)
|
||||
// }
|
||||
func Equal[T any](expected T) Kleisli[T] {
|
||||
return wrap1(assert.Equal, expected)
|
||||
}
|
||||
|
||||
// ArrayNotEmpty checks if an array is not empty
|
||||
// ArrayNotEmpty checks if an array is not empty.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestArrayNotEmpty(t *testing.T) {
|
||||
// numbers := []int{1, 2, 3}
|
||||
// assert.ArrayNotEmpty(numbers)(t) // Passes
|
||||
//
|
||||
// empty := []int{}
|
||||
// assert.ArrayNotEmpty(empty)(t) // Fails
|
||||
// }
|
||||
func ArrayNotEmpty[T any](arr []T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.NotEmpty(t, arr)
|
||||
}
|
||||
}
|
||||
|
||||
// RecordNotEmpty checks if an map is not empty
|
||||
// RecordNotEmpty checks if a map is not empty.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestRecordNotEmpty(t *testing.T) {
|
||||
// config := map[string]int{"timeout": 30, "retries": 3}
|
||||
// assert.RecordNotEmpty(config)(t) // Passes
|
||||
//
|
||||
// empty := map[string]int{}
|
||||
// assert.RecordNotEmpty(empty)(t) // Fails
|
||||
// }
|
||||
func RecordNotEmpty[K comparable, T any](mp map[K]T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.NotEmpty(t, mp)
|
||||
}
|
||||
}
|
||||
|
||||
// StringNotEmpty checks if a string is not empty
|
||||
// StringNotEmpty checks if a string is not empty.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestStringNotEmpty(t *testing.T) {
|
||||
// message := "Hello, World!"
|
||||
// assert.StringNotEmpty(message)(t) // Passes
|
||||
//
|
||||
// empty := ""
|
||||
// assert.StringNotEmpty(empty)(t) // Fails
|
||||
// }
|
||||
func StringNotEmpty(s string) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.NotEmpty(t, s)
|
||||
}
|
||||
}
|
||||
|
||||
// ArrayLength tests if an array has the expected length
|
||||
// ArrayLength tests if an array has the expected length.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestArrayLength(t *testing.T) {
|
||||
// numbers := []int{1, 2, 3, 4, 5}
|
||||
// assert.ArrayLength[int](5)(numbers)(t) // Passes
|
||||
// assert.ArrayLength[int](3)(numbers)(t) // Fails
|
||||
// }
|
||||
func ArrayLength[T any](expected int) Kleisli[[]T] {
|
||||
return func(actual []T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
@@ -123,7 +245,15 @@ func ArrayLength[T any](expected int) Kleisli[[]T] {
|
||||
}
|
||||
}
|
||||
|
||||
// RecordLength tests if a map has the expected length
|
||||
// RecordLength tests if a map has the expected length.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestRecordLength(t *testing.T) {
|
||||
// config := map[string]string{"host": "localhost", "port": "8080"}
|
||||
// assert.RecordLength[string, string](2)(config)(t) // Passes
|
||||
// assert.RecordLength[string, string](3)(config)(t) // Fails
|
||||
// }
|
||||
func RecordLength[K comparable, T any](expected int) Kleisli[map[K]T] {
|
||||
return func(actual map[K]T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
@@ -132,7 +262,15 @@ func RecordLength[K comparable, T any](expected int) Kleisli[map[K]T] {
|
||||
}
|
||||
}
|
||||
|
||||
// StringLength tests if a string has the expected length
|
||||
// StringLength tests if a string has the expected length.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestStringLength(t *testing.T) {
|
||||
// message := "Hello"
|
||||
// assert.StringLength[any, any](5)(message)(t) // Passes
|
||||
// assert.StringLength[any, any](10)(message)(t) // Fails
|
||||
// }
|
||||
func StringLength[K comparable, T any](expected int) Kleisli[string] {
|
||||
return func(actual string) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
@@ -141,31 +279,93 @@ func StringLength[K comparable, T any](expected int) Kleisli[string] {
|
||||
}
|
||||
}
|
||||
|
||||
// NoError validates that there is no error
|
||||
// NoError validates that there is no error.
|
||||
//
|
||||
// This is commonly used to assert that operations complete successfully.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestNoError(t *testing.T) {
|
||||
// err := doSomething()
|
||||
// assert.NoError(err)(t) // Passes if err is nil
|
||||
//
|
||||
// // Can be used with result types
|
||||
// result := result.TryCatch(func() (int, error) {
|
||||
// return 42, nil
|
||||
// })
|
||||
// assert.Success(result)(t) // Uses NoError internally
|
||||
// }
|
||||
func NoError(err error) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Error validates that there is an error
|
||||
// Error validates that there is an error.
|
||||
//
|
||||
// This is used to assert that operations fail as expected.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestError(t *testing.T) {
|
||||
// err := validateInput("")
|
||||
// assert.Error(err)(t) // Passes if err is not nil
|
||||
//
|
||||
// err2 := validateInput("valid")
|
||||
// assert.Error(err2)(t) // Fails if err2 is nil
|
||||
// }
|
||||
func Error(err error) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Success checks if a [Result] represents success
|
||||
// Success checks if a [Result] represents success.
|
||||
//
|
||||
// This is a convenience function for testing Result types from the fp-go library.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestSuccess(t *testing.T) {
|
||||
// res := result.Of[int](42)
|
||||
// assert.Success(res)(t) // Passes
|
||||
//
|
||||
// failedRes := result.Error[int](errors.New("failed"))
|
||||
// assert.Success(failedRes)(t) // Fails
|
||||
// }
|
||||
func Success[T any](res Result[T]) Reader {
|
||||
return NoError(result.ToError(res))
|
||||
}
|
||||
|
||||
// Failure checks if a [Result] represents failure
|
||||
// Failure checks if a [Result] represents failure.
|
||||
//
|
||||
// This is a convenience function for testing Result types from the fp-go library.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestFailure(t *testing.T) {
|
||||
// res := result.Error[int](errors.New("something went wrong"))
|
||||
// assert.Failure(res)(t) // Passes
|
||||
//
|
||||
// successRes := result.Of[int](42)
|
||||
// assert.Failure(successRes)(t) // Fails
|
||||
// }
|
||||
func Failure[T any](res Result[T]) Reader {
|
||||
return Error(result.ToError(res))
|
||||
}
|
||||
|
||||
// ArrayContains tests if a value is contained in an array
|
||||
// ArrayContains tests if a value is contained in an array.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestArrayContains(t *testing.T) {
|
||||
// numbers := []int{1, 2, 3, 4, 5}
|
||||
// assert.ArrayContains(3)(numbers)(t) // Passes
|
||||
// assert.ArrayContains(10)(numbers)(t) // Fails
|
||||
//
|
||||
// names := []string{"Alice", "Bob", "Charlie"}
|
||||
// assert.ArrayContains("Bob")(names)(t) // Passes
|
||||
// }
|
||||
func ArrayContains[T any](expected T) Kleisli[[]T] {
|
||||
return func(actual []T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
@@ -174,7 +374,15 @@ func ArrayContains[T any](expected T) Kleisli[[]T] {
|
||||
}
|
||||
}
|
||||
|
||||
// ContainsKey tests if a key is contained in a map
|
||||
// ContainsKey tests if a key is contained in a map.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestContainsKey(t *testing.T) {
|
||||
// config := map[string]int{"timeout": 30, "retries": 3}
|
||||
// assert.ContainsKey[int]("timeout")(config)(t) // Passes
|
||||
// assert.ContainsKey[int]("maxSize")(config)(t) // Fails
|
||||
// }
|
||||
func ContainsKey[T any, K comparable](expected K) Kleisli[map[K]T] {
|
||||
return func(actual map[K]T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
@@ -183,7 +391,15 @@ func ContainsKey[T any, K comparable](expected K) Kleisli[map[K]T] {
|
||||
}
|
||||
}
|
||||
|
||||
// NotContainsKey tests if a key is not contained in a map
|
||||
// NotContainsKey tests if a key is not contained in a map.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestNotContainsKey(t *testing.T) {
|
||||
// config := map[string]int{"timeout": 30, "retries": 3}
|
||||
// assert.NotContainsKey[int]("maxSize")(config)(t) // Passes
|
||||
// assert.NotContainsKey[int]("timeout")(config)(t) // Fails
|
||||
// }
|
||||
func NotContainsKey[T any, K comparable](expected K) Kleisli[map[K]T] {
|
||||
return func(actual map[K]T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
@@ -192,7 +408,31 @@ func NotContainsKey[T any, K comparable](expected K) Kleisli[map[K]T] {
|
||||
}
|
||||
}
|
||||
|
||||
// That asserts that a particular predicate matches
|
||||
// That asserts that a particular predicate matches.
|
||||
//
|
||||
// This is a powerful function that allows you to create custom assertions using predicates.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestThat(t *testing.T) {
|
||||
// // Test if a number is positive
|
||||
// isPositive := func(n int) bool { return n > 0 }
|
||||
// assert.That(isPositive)(42)(t) // Passes
|
||||
// assert.That(isPositive)(-5)(t) // Fails
|
||||
//
|
||||
// // Test if a string is uppercase
|
||||
// isUppercase := func(s string) bool { return s == strings.ToUpper(s) }
|
||||
// assert.That(isUppercase)("HELLO")(t) // Passes
|
||||
// assert.That(isUppercase)("Hello")(t) // Fails
|
||||
//
|
||||
// // Can be combined with Local for property testing
|
||||
// type User struct { Age int }
|
||||
// ageIsAdult := assert.Local(func(u User) int { return u.Age })(
|
||||
// assert.That(func(age int) bool { return age >= 18 }),
|
||||
// )
|
||||
// user := User{Age: 25}
|
||||
// ageIsAdult(user)(t) // Passes
|
||||
// }
|
||||
func That[T any](pred Predicate[T]) Kleisli[T] {
|
||||
return func(a T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
|
||||
237
v2/assert/example_test.go
Normal file
237
v2/assert/example_test.go
Normal 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
|
||||
@@ -22,13 +22,14 @@ 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/IBM/fp-go/v2/reader"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMonadMap(t *testing.T) {
|
||||
rio := Of(5)
|
||||
doubled := MonadMap(rio, func(n int) int { return n * 2 })
|
||||
doubled := MonadMap(rio, N.Mul(2))
|
||||
|
||||
result := doubled(context.Background())()
|
||||
assert.Equal(t, 10, result)
|
||||
@@ -144,7 +145,7 @@ func TestOf(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadAp(t *testing.T) {
|
||||
fabIO := Of(func(n int) int { return n * 2 })
|
||||
fabIO := Of(N.Mul(2))
|
||||
faIO := Of(5)
|
||||
result := MonadAp(fabIO, faIO)
|
||||
|
||||
@@ -161,7 +162,7 @@ func TestAp(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadApSeq(t *testing.T) {
|
||||
fabIO := Of(func(n int) int { return n + 10 })
|
||||
fabIO := Of(N.Add(10))
|
||||
faIO := Of(5)
|
||||
result := MonadApSeq(fabIO, faIO)
|
||||
|
||||
@@ -170,7 +171,7 @@ func TestMonadApSeq(t *testing.T) {
|
||||
|
||||
func TestApSeq(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Of(func(n int) int { return n + 10 }),
|
||||
Of(N.Add(10)),
|
||||
ApSeq[int](Of(5)),
|
||||
)
|
||||
|
||||
@@ -178,7 +179,7 @@ func TestApSeq(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadApPar(t *testing.T) {
|
||||
fabIO := Of(func(n int) int { return n + 10 })
|
||||
fabIO := Of(N.Add(10))
|
||||
faIO := Of(5)
|
||||
result := MonadApPar(fabIO, faIO)
|
||||
|
||||
@@ -187,7 +188,7 @@ func TestMonadApPar(t *testing.T) {
|
||||
|
||||
func TestApPar(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Of(func(n int) int { return n + 10 }),
|
||||
Of(N.Add(10)),
|
||||
ApPar[int](Of(5)),
|
||||
)
|
||||
|
||||
@@ -343,7 +344,7 @@ func TestFlatten(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadFlap(t *testing.T) {
|
||||
fabIO := Of(func(n int) int { return n * 3 })
|
||||
fabIO := Of(N.Mul(3))
|
||||
result := MonadFlap(fabIO, 7)
|
||||
|
||||
assert.Equal(t, 21, result(context.Background())())
|
||||
@@ -351,7 +352,7 @@ func TestMonadFlap(t *testing.T) {
|
||||
|
||||
func TestFlap(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of(func(n int) int { return n * 3 }),
|
||||
Of(N.Mul(3)),
|
||||
Flap[int](7),
|
||||
)
|
||||
|
||||
@@ -459,7 +460,7 @@ func TestComplexPipeline(t *testing.T) {
|
||||
Chain(func(n int) ReaderIO[int] {
|
||||
return Of(n * 2)
|
||||
}),
|
||||
Map(func(n int) int { return n + 10 }),
|
||||
Map(N.Add(10)),
|
||||
)
|
||||
|
||||
assert.Equal(t, 20, result(context.Background())()) // (5 * 2) + 10 = 20
|
||||
@@ -488,7 +489,7 @@ func TestTapWithLogging(t *testing.T) {
|
||||
logged = append(logged, n)
|
||||
return Of(func() {})
|
||||
}),
|
||||
Map(func(n int) int { return n * 2 }),
|
||||
Map(N.Mul(2)),
|
||||
Tap(func(n int) ReaderIO[func()] {
|
||||
logged = append(logged, n)
|
||||
return Of(func() {})
|
||||
|
||||
@@ -47,7 +47,23 @@ 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]
|
||||
)
|
||||
|
||||
@@ -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]),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
441
v2/endomorphism/from_test.go
Normal file
441
v2/endomorphism/from_test.go
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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) }),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -72,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 {
|
||||
@@ -85,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)
|
||||
@@ -285,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)
|
||||
|
||||
@@ -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]
|
||||
```
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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)]
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -166,7 +166,7 @@ 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)
|
||||
@@ -214,7 +214,7 @@ func MonadMapTo[R, A, B any](fa ReaderIO[R, A], b 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)
|
||||
@@ -449,7 +449,7 @@ 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
|
||||
@@ -508,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] {
|
||||
@@ -846,7 +846,7 @@ 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, 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)
|
||||
@@ -869,7 +869,7 @@ func MonadFlap[R, B, A 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
|
||||
//
|
||||
|
||||
@@ -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"}
|
||||
@@ -551,7 +552,7 @@ func TestTapWithLogging(t *testing.T) {
|
||||
logged = append(logged, n)
|
||||
return Of[ReaderTestConfig](func() {})
|
||||
}),
|
||||
Map[ReaderTestConfig](func(n int) int { return n * 2 }),
|
||||
Map[ReaderTestConfig](N.Mul(2)),
|
||||
Tap(func(n int) ReaderIO[ReaderTestConfig, func()] {
|
||||
logged = append(logged, n)
|
||||
return Of[ReaderTestConfig](func() {})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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]] {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user