1
0
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:
Dr. Carsten Leue
2025-12-04 16:31:21 +01:00
parent ff48d8953e
commit 24c0519cc7
33 changed files with 2025 additions and 313 deletions

574
v2/DESIGN.md Normal file
View File

@@ -0,0 +1,574 @@
# Design Decisions
This document explains the key design decisions and principles behind fp-go's API design.
## Table of Contents
- [Data Last Principle](#data-last-principle)
- [Kleisli and Operator Types](#kleisli-and-operator-types)
- [Monadic Operations Comparison](#monadic-operations-comparison)
- [Type Parameter Ordering](#type-parameter-ordering)
- [Generic Type Aliases](#generic-type-aliases)
## Data Last Principle
fp-go follows the **"data last"** principle, where the data being operated on is always the last parameter in a function. This design choice enables powerful function composition and partial application patterns.
### What is "Data Last"?
In the "data last" style, functions are structured so that:
1. Configuration parameters come first
2. The data to be transformed comes last
This is the opposite of the traditional object-oriented style where the data (receiver) comes first.
### Why "Data Last"?
The "data last" principle enables:
1. **Natural Currying**: Functions can be partially applied to create specialized transformations
2. **Function Composition**: Operations can be composed before applying them to data
3. **Point-Free Style**: Write transformations without explicitly mentioning the data
4. **Reusability**: Create reusable transformation pipelines
### Examples
#### Basic Transformation
```go
// Data last style (fp-go)
double := array.Map(number.Mul(2))
result := double([]int{1, 2, 3}) // [2, 4, 6]
// Compare with data first style (traditional)
result := array.Map([]int{1, 2, 3}, number.Mul(2))
```
#### Function Composition
```go
import (
A "github.com/IBM/fp-go/v2/array"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
)
// Create a pipeline of transformations
pipeline := F.Flow3(
A.Filter(func(x int) bool { return x > 0 }), // Keep positive numbers
A.Map(N.Mul(2)), // Double each number
A.Reduce(func(acc, x int) int { return acc + x }, 0), // Sum them up
)
// Apply the pipeline to different data
result1 := pipeline([]int{-1, 2, 3, -4, 5}) // (2 + 3 + 5) * 2 = 20
result2 := pipeline([]int{1, 2, 3}) // (1 + 2 + 3) * 2 = 12
```
#### Partial Application
```go
import (
O "github.com/IBM/fp-go/v2/option"
)
// Create specialized functions by partial application
getOrZero := O.GetOrElse(func() int { return 0 })
getOrEmpty := O.GetOrElse(func() string { return "" })
// Use them with different data
value1 := getOrZero(O.Some(42)) // 42
value2 := getOrZero(O.None[int]()) // 0
text1 := getOrEmpty(O.Some("hello")) // "hello"
text2 := getOrEmpty(O.None[string]()) // ""
```
#### Building Reusable Transformations
```go
import (
E "github.com/IBM/fp-go/v2/either"
O "github.com/IBM/fp-go/v2/option"
)
// Create a reusable validation pipeline
type User struct {
Name string
Email string
Age int
}
validateAge := E.FromPredicate(
func(u User) bool { return u.Age >= 18 },
func(u User) error { return errors.New("must be 18 or older") },
)
validateEmail := E.FromPredicate(
func(u User) bool { return strings.Contains(u.Email, "@") },
func(u User) error { return errors.New("invalid email") },
)
// Compose validators
validateUser := F.Flow2(
validateAge,
E.Chain(validateEmail),
)
// Apply to different users
result1 := validateUser(User{Name: "Alice", Email: "alice@example.com", Age: 25})
result2 := validateUser(User{Name: "Bob", Email: "invalid", Age: 30})
```
#### Monadic Operations
```go
import (
O "github.com/IBM/fp-go/v2/option"
)
// Data last enables clean monadic chains
parseAndDouble := F.Flow2(
O.FromPredicate(func(s string) bool { return s != "" }),
O.Chain(func(s string) O.Option[int] {
n, err := strconv.Atoi(s)
if err != nil {
return O.None[int]()
}
return O.Some(n * 2)
}),
)
result1 := parseAndDouble("21") // Some(42)
result2 := parseAndDouble("") // None
result3 := parseAndDouble("abc") // None
```
### Monadic vs Non-Monadic Forms
fp-go provides two forms for most operations:
1. **Curried form** (data last): Returns a function that can be composed
2. **Monadic form** (data first): Takes all parameters at once
```go
// Curried form - data last, returns a function
Map[A, B any](f func(A) B) func(Option[A]) Option[B]
// Monadic form - data first, direct execution
MonadMap[A, B any](fa Option[A], f func(A) B) Option[B]
```
**When to use each:**
- **Curried form**: When building pipelines, composing functions, or creating reusable transformations
- **Monadic form**: When you have all parameters available and want direct execution
```go
// Curried form - building a pipeline
transform := F.Flow3(
O.Map(strings.ToUpper),
O.Filter(func(s string) bool { return len(s) > 3 }),
O.GetOrElse(func() string { return "DEFAULT" }),
)
result := transform(O.Some("hello"))
// Monadic form - direct execution
result := O.MonadMap(O.Some("hello"), strings.ToUpper)
```
### Further Reading on Data-Last Pattern
The data-last currying pattern is well-documented in the functional programming community:
- [Mostly Adequate Guide - Ch. 4: Currying](https://mostly-adequate.gitbook.io/mostly-adequate-guide/ch04) - Excellent introduction with clear examples
- [Curry and Function Composition](https://medium.com/javascript-scene/curry-and-function-composition-2c208d774983) by Eric Elliott
- [fp-ts Issue #1238](https://github.com/gcanti/fp-ts/issues/1238) - Real-world examples of data-last refactoring
## Kleisli and Operator Types
fp-go uses consistent type aliases across all monads to make code more recognizable and composable. These types provide a common vocabulary that works across different monadic contexts.
### Type Definitions
```go
// Kleisli arrow - a function that returns a monadic value
type Kleisli[A, B any] = func(A) M[B]
// Operator - a function that transforms a monadic value
type Operator[A, B any] = func(M[A]) M[B]
```
Where `M` represents the specific monad (Option, Either, IO, etc.).
### Why These Types Matter
1. **Consistency**: The same type names appear across all monads
2. **Recognizability**: Experienced functional programmers immediately understand the intent
3. **Composability**: Functions with these types compose naturally
4. **Documentation**: Type signatures clearly communicate the operation's behavior
### Examples Across Monads
#### Option Monad
```go
// option/option.go
type Kleisli[A, B any] = func(A) Option[B]
type Operator[A, B any] = func(Option[A]) Option[B]
// Chain uses Kleisli
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B]
// Map returns an Operator
func Map[A, B any](f func(A) B) Operator[A, B]
```
#### Either Monad
```go
// either/either.go
type Kleisli[E, A, B any] = func(A) Either[E, B]
type Operator[E, A, B any] = func(Either[E, A]) Either[E, B]
// Chain uses Kleisli
func Chain[E, A, B any](f Kleisli[E, A, B]) Operator[E, A, B]
// Map returns an Operator
func Map[E, A, B any](f func(A) B) Operator[E, A, B]
```
#### IO Monad
```go
// io/io.go
type Kleisli[A, B any] = func(A) IO[B]
type Operator[A, B any] = func(IO[A]) IO[B]
// Chain uses Kleisli
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B]
// Map returns an Operator
func Map[A, B any](f func(A) B) Operator[A, B]
```
#### Array (List Monad)
```go
// array/array.go
type Kleisli[A, B any] = func(A) []B
type Operator[A, B any] = func([]A) []B
// Chain uses Kleisli
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B]
// Map returns an Operator
func Map[A, B any](f func(A) B) Operator[A, B]
```
### Pattern Recognition
Once you learn these patterns in one monad, you can apply them to all monads:
```go
// The pattern is always the same, just the monad changes
// Option
validateAge := option.Chain(func(user User) option.Option[User] {
if user.Age >= 18 {
return option.Some(user)
}
return option.None[User]()
})
// Either
validateAge := either.Chain(func(user User) either.Either[error, User] {
if user.Age >= 18 {
return either.Right[error](user)
}
return either.Left[User](errors.New("too young"))
})
// IO
validateAge := io.Chain(func(user User) io.IO[User] {
return io.Of(user) // Always succeeds in IO
})
// Array
validateAge := array.Chain(func(user User) []User {
if user.Age >= 18 {
return []User{user}
}
return []User{} // Empty array = failure
})
```
### Composing Kleisli Arrows
Kleisli arrows compose naturally using monadic composition:
```go
import (
O "github.com/IBM/fp-go/v2/option"
F "github.com/IBM/fp-go/v2/function"
)
// Define Kleisli arrows
parseAge := func(s string) O.Option[int] {
n, err := strconv.Atoi(s)
if err != nil {
return O.None[int]()
}
return O.Some(n)
}
validateAge := func(age int) O.Option[int] {
if age >= 18 {
return O.Some(age)
}
return O.None[int]()
}
formatAge := func(age int) O.Option[string] {
return O.Some(fmt.Sprintf("Age: %d", age))
}
// Compose them using Flow and Chain
pipeline := F.Flow3(
parseAge,
O.Chain(validateAge),
O.Chain(formatAge),
)
result := pipeline("25") // Some("Age: 25")
result := pipeline("15") // None (too young)
result := pipeline("abc") // None (parse error)
```
### Building Reusable Operators
Operators can be created once and reused across your codebase:
```go
import (
E "github.com/IBM/fp-go/v2/either"
)
// Create reusable operators
type ValidationError struct {
Field string
Message string
}
// Reusable validation operators
validateNonEmpty := E.Chain(func(s string) E.Either[ValidationError, string] {
if s == "" {
return E.Left[string](ValidationError{
Field: "input",
Message: "cannot be empty",
})
}
return E.Right[ValidationError](s)
})
validateEmail := E.Chain(func(s string) E.Either[ValidationError, string] {
if !strings.Contains(s, "@") {
return E.Left[string](ValidationError{
Field: "email",
Message: "invalid format",
})
}
return E.Right[ValidationError](s)
})
// Compose operators
validateEmailInput := F.Flow2(
validateNonEmpty,
validateEmail,
)
// Use across your application
result1 := validateEmailInput(E.Right[ValidationError]("user@example.com"))
result2 := validateEmailInput(E.Right[ValidationError](""))
result3 := validateEmailInput(E.Right[ValidationError]("invalid"))
```
### Benefits of Consistent Naming
1. **Cross-monad understanding**: Learn once, apply everywhere
2. **Easier refactoring**: Changing monads requires minimal code changes
3. **Better tooling**: IDEs can provide better suggestions
4. **Team communication**: Shared vocabulary across the team
5. **Library integration**: Third-party libraries follow the same patterns
### Identity Monad - The Simplest Case
The Identity monad shows these types in their simplest form:
```go
// identity/doc.go
type Operator[A, B any] = func(A) B
// In Identity, there's no wrapping, so:
// - Kleisli[A, B] is just func(A) B
// - Operator[A, B] is just func(A) B
// They're the same because Identity adds no context
```
This demonstrates that these type aliases represent fundamental functional programming concepts, not just arbitrary naming conventions.
## Monadic Operations Comparison
fp-go's monadic operations are inspired by functional programming languages and libraries. Here's how they compare:
| fp-go | fp-ts | Haskell | Scala | Description |
|-------|-------|---------|-------|-------------|
| `Map` | `map` | `fmap` | `map` | Functor mapping - transforms the value inside a context |
| `Chain` | `chain` | `>>=` (bind) | `flatMap` | Monadic bind - chains computations that return wrapped values |
| `Ap` | `ap` | `<*>` | `ap` | Applicative apply - applies a wrapped function to a wrapped value |
| `Of` | `of` | `return`/`pure` | `pure` | Lifts a pure value into a monadic context |
| `Fold` | `fold` | `either` | `fold` | Eliminates the context by providing handlers for each case |
| `Filter` | `filter` | `mfilter` | `filter` | Keeps values that satisfy a predicate |
| `Flatten` | `flatten` | `join` | `flatten` | Removes one level of nesting |
| `ChainFirst` | `chainFirst` | `>>` (then) | `tap` | Chains for side effects, keeping the original value |
| `Alt` | `alt` | `<\|>` | `orElse` | Provides an alternative value if the first fails |
| `GetOrElse` | `getOrElse` | `fromMaybe` | `getOrElse` | Extracts the value or provides a default |
| `FromPredicate` | `fromPredicate` | `guard` | `filter` | Creates a monadic value based on a predicate |
| `Sequence` | `sequence` | `sequence` | `sequence` | Transforms a collection of effects into an effect of a collection |
| `Traverse` | `traverse` | `traverse` | `traverse` | Maps and sequences in one operation |
| `Reduce` | `reduce` | `foldl` | `foldLeft` | Folds a structure from left to right |
| `ReduceRight` | `reduceRight` | `foldr` | `foldRight` | Folds a structure from right to left |
### Key Differences from Other Languages
#### Naming Conventions
- **Go conventions**: fp-go uses PascalCase for exported functions (e.g., `Map`, `Chain`) following Go's naming conventions
- **Type parameters first**: Non-inferrable type parameters come first (e.g., `Ap[B, E, A any]`)
- **Monadic prefix**: Direct execution forms use the `Monad` prefix (e.g., `MonadMap`, `MonadChain`)
#### Type System
```go
// fp-go (explicit type parameters when needed)
result := option.Map(transform)(value)
result := option.Map[string, int](transform)(value) // explicit when inference fails
// Haskell (type inference)
result = fmap transform value
// Scala (type inference with method syntax)
result = value.map(transform)
// fp-ts (TypeScript type inference)
const result = pipe(value, map(transform))
```
#### Currying
```go
// fp-go - explicit currying with data last
double := array.Map(number.Mul(2))
result := double(numbers)
// Haskell - automatic currying
double = fmap (*2)
result = double numbers
// Scala - method syntax
result = numbers.map(_ * 2)
```
## Type Parameter Ordering
fp-go v2 uses a specific ordering for type parameters to maximize type inference:
### Rule: Non-Inferrable Parameters First
Type parameters that **cannot be inferred** from function arguments come first. This allows the Go compiler to infer as many types as possible.
```go
// Ap - B cannot be inferred from arguments, so it comes first
func Ap[B, E, A any](fa Either[E, A]) func(Either[E, func(A) B]) Either[E, B]
// Usage - only B needs to be specified
result := either.Ap[string](value)(funcInEither)
```
### Examples
```go
// Map - all types can be inferred from arguments
func Map[E, A, B any](f func(A) B) func(Either[E, A]) Either[E, B]
// Usage - no type parameters needed
result := either.Map(transform)(value)
// Chain - all types can be inferred
func Chain[E, A, B any](f func(A) Either[E, B]) func(Either[E, A]) Either[E, B]
// Usage - no type parameters needed
result := either.Chain(validator)(value)
// Of - E cannot be inferred, comes first
func Of[E, A any](value A) Either[E, A]
// Usage - only E needs to be specified
result := either.Of[error](42)
```
### Benefits
1. **Less verbose code**: Most operations don't require explicit type parameters
2. **Better IDE support**: Type inference provides better autocomplete
3. **Clearer intent**: Only specify types that can't be inferred
## Generic Type Aliases
fp-go v2 leverages Go 1.24's generic type aliases for cleaner type definitions:
```go
// V2 - using generic type alias (requires Go 1.24+)
type ReaderIOEither[R, E, A any] = RD.Reader[R, IOE.IOEither[E, A]]
// V1 - using type definition (Go 1.18+)
type ReaderIOEither[R, E, A any] RD.Reader[R, IOE.IOEither[E, A]]
```
### Benefits
1. **True aliases**: The type is interchangeable with its definition
2. **No namespace imports needed**: Can use types directly without package prefixes
3. **Simpler codebase**: Eliminates the need for `generic` subpackages
4. **Better composability**: Types compose more naturally
### Migration Pattern
```go
// Define project-wide aliases once
package types
import (
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/ioresult"
)
type Option[A any] = option.Option[A]
type Result[A any] = result.Result[A]
type IOResult[A any] = ioresult.IOResult[A]
// Use throughout your codebase
package myapp
import "myproject/types"
func process(input string) types.Result[types.Option[int]] {
// implementation
}
```
---
For more information, see:
- [README.md](./README.md) - Overview and quick start
- [API Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2) - Complete API reference
- [Samples](./samples/) - Practical examples

View File

@@ -19,6 +19,58 @@
// allowing for composable and functional test assertions. Each assertion
// returns a Reader that takes a *testing.T and performs the assertion.
//
// # Data Last Principle
//
// This package follows the "data last" functional programming principle, where
// the data being operated on comes as the last parameter in a chain of function
// applications. This design enables several powerful functional programming patterns:
//
// 1. **Partial Application**: You can create reusable assertion functions by providing
// configuration parameters first, leaving the data and testing context for later.
//
// 2. **Function Composition**: Assertions can be composed and combined before being
// applied to actual data.
//
// 3. **Point-Free Style**: You can pass assertion functions around without immediately
// providing the data they operate on.
//
// The general pattern is:
//
// assert.Function(config)(data)(testingContext)
// ↑ ↑ ↑
// expected actual *testing.T (always last)
//
// For single-parameter assertions:
//
// assert.Function(data)(testingContext)
// ↑ ↑
// actual *testing.T (always last)
//
// Examples of "data last" in action:
//
// // Multi-parameter: expected value → actual value → testing context
// assert.Equal(42)(result)(t)
// assert.ArrayContains(3)(numbers)(t)
//
// // Single-parameter: data → testing context
// assert.NoError(err)(t)
// assert.ArrayNotEmpty(arr)(t)
//
// // Partial application - create reusable assertions
// isPositive := assert.That(func(n int) bool { return n > 0 })
// // Later, apply to different values:
// isPositive(42)(t) // Passes
// isPositive(-5)(t) // Fails
//
// // Composition - combine assertions before applying data
// validateUser := func(u User) assert.Reader {
// return assert.AllOf([]assert.Reader{
// assert.Equal("Alice")(u.Name),
// assert.That(func(age int) bool { return age >= 18 })(u.Age),
// })
// }
// validateUser(user)(t)
//
// The package supports:
// - Equality and inequality assertions
// - Collection assertions (arrays, maps, strings)
@@ -83,38 +135,108 @@ func wrap1[T any](wrapped func(t assert.TestingT, expected, actual any, msgAndAr
}
}
// NotEqual tests if the expected and the actual values are not equal
// NotEqual tests if the expected and the actual values are not equal.
//
// This function follows the "data last" principle - you provide the expected value first,
// then the actual value, and finally the testing.T context.
//
// Example:
//
// func TestNotEqual(t *testing.T) {
// value := 42
// assert.NotEqual(10)(value)(t) // Passes: 42 != 10
// assert.NotEqual(42)(value)(t) // Fails: 42 == 42
// }
func NotEqual[T any](expected T) Kleisli[T] {
return wrap1(assert.NotEqual, expected)
}
// Equal tests if the expected and the actual values are equal
// Equal tests if the expected and the actual values are equal.
//
// This is one of the most commonly used assertions. It follows the "data last" principle -
// you provide the expected value first, then the actual value, and finally the testing.T context.
//
// Example:
//
// func TestEqual(t *testing.T) {
// result := 2 + 2
// assert.Equal(4)(result)(t) // Passes
//
// name := "Alice"
// assert.Equal("Alice")(name)(t) // Passes
//
// // Can be composed with other assertions
// user := User{Name: "Bob", Age: 30}
// assertions := assert.AllOf([]assert.Reader{
// assert.Equal("Bob")(user.Name),
// assert.Equal(30)(user.Age),
// })
// assertions(t)
// }
func Equal[T any](expected T) Kleisli[T] {
return wrap1(assert.Equal, expected)
}
// ArrayNotEmpty checks if an array is not empty
// ArrayNotEmpty checks if an array is not empty.
//
// Example:
//
// func TestArrayNotEmpty(t *testing.T) {
// numbers := []int{1, 2, 3}
// assert.ArrayNotEmpty(numbers)(t) // Passes
//
// empty := []int{}
// assert.ArrayNotEmpty(empty)(t) // Fails
// }
func ArrayNotEmpty[T any](arr []T) Reader {
return func(t *testing.T) bool {
return assert.NotEmpty(t, arr)
}
}
// RecordNotEmpty checks if an map is not empty
// RecordNotEmpty checks if a map is not empty.
//
// Example:
//
// func TestRecordNotEmpty(t *testing.T) {
// config := map[string]int{"timeout": 30, "retries": 3}
// assert.RecordNotEmpty(config)(t) // Passes
//
// empty := map[string]int{}
// assert.RecordNotEmpty(empty)(t) // Fails
// }
func RecordNotEmpty[K comparable, T any](mp map[K]T) Reader {
return func(t *testing.T) bool {
return assert.NotEmpty(t, mp)
}
}
// StringNotEmpty checks if a string is not empty
// StringNotEmpty checks if a string is not empty.
//
// Example:
//
// func TestStringNotEmpty(t *testing.T) {
// message := "Hello, World!"
// assert.StringNotEmpty(message)(t) // Passes
//
// empty := ""
// assert.StringNotEmpty(empty)(t) // Fails
// }
func StringNotEmpty(s string) Reader {
return func(t *testing.T) bool {
return assert.NotEmpty(t, s)
}
}
// ArrayLength tests if an array has the expected length
// ArrayLength tests if an array has the expected length.
//
// Example:
//
// func TestArrayLength(t *testing.T) {
// numbers := []int{1, 2, 3, 4, 5}
// assert.ArrayLength[int](5)(numbers)(t) // Passes
// assert.ArrayLength[int](3)(numbers)(t) // Fails
// }
func ArrayLength[T any](expected int) Kleisli[[]T] {
return func(actual []T) Reader {
return func(t *testing.T) bool {
@@ -123,7 +245,15 @@ func ArrayLength[T any](expected int) Kleisli[[]T] {
}
}
// RecordLength tests if a map has the expected length
// RecordLength tests if a map has the expected length.
//
// Example:
//
// func TestRecordLength(t *testing.T) {
// config := map[string]string{"host": "localhost", "port": "8080"}
// assert.RecordLength[string, string](2)(config)(t) // Passes
// assert.RecordLength[string, string](3)(config)(t) // Fails
// }
func RecordLength[K comparable, T any](expected int) Kleisli[map[K]T] {
return func(actual map[K]T) Reader {
return func(t *testing.T) bool {
@@ -132,7 +262,15 @@ func RecordLength[K comparable, T any](expected int) Kleisli[map[K]T] {
}
}
// StringLength tests if a string has the expected length
// StringLength tests if a string has the expected length.
//
// Example:
//
// func TestStringLength(t *testing.T) {
// message := "Hello"
// assert.StringLength[any, any](5)(message)(t) // Passes
// assert.StringLength[any, any](10)(message)(t) // Fails
// }
func StringLength[K comparable, T any](expected int) Kleisli[string] {
return func(actual string) Reader {
return func(t *testing.T) bool {
@@ -141,31 +279,93 @@ func StringLength[K comparable, T any](expected int) Kleisli[string] {
}
}
// NoError validates that there is no error
// NoError validates that there is no error.
//
// This is commonly used to assert that operations complete successfully.
//
// Example:
//
// func TestNoError(t *testing.T) {
// err := doSomething()
// assert.NoError(err)(t) // Passes if err is nil
//
// // Can be used with result types
// result := result.TryCatch(func() (int, error) {
// return 42, nil
// })
// assert.Success(result)(t) // Uses NoError internally
// }
func NoError(err error) Reader {
return func(t *testing.T) bool {
return assert.NoError(t, err)
}
}
// Error validates that there is an error
// Error validates that there is an error.
//
// This is used to assert that operations fail as expected.
//
// Example:
//
// func TestError(t *testing.T) {
// err := validateInput("")
// assert.Error(err)(t) // Passes if err is not nil
//
// err2 := validateInput("valid")
// assert.Error(err2)(t) // Fails if err2 is nil
// }
func Error(err error) Reader {
return func(t *testing.T) bool {
return assert.Error(t, err)
}
}
// Success checks if a [Result] represents success
// Success checks if a [Result] represents success.
//
// This is a convenience function for testing Result types from the fp-go library.
//
// Example:
//
// func TestSuccess(t *testing.T) {
// res := result.Of[int](42)
// assert.Success(res)(t) // Passes
//
// failedRes := result.Error[int](errors.New("failed"))
// assert.Success(failedRes)(t) // Fails
// }
func Success[T any](res Result[T]) Reader {
return NoError(result.ToError(res))
}
// Failure checks if a [Result] represents failure
// Failure checks if a [Result] represents failure.
//
// This is a convenience function for testing Result types from the fp-go library.
//
// Example:
//
// func TestFailure(t *testing.T) {
// res := result.Error[int](errors.New("something went wrong"))
// assert.Failure(res)(t) // Passes
//
// successRes := result.Of[int](42)
// assert.Failure(successRes)(t) // Fails
// }
func Failure[T any](res Result[T]) Reader {
return Error(result.ToError(res))
}
// ArrayContains tests if a value is contained in an array
// ArrayContains tests if a value is contained in an array.
//
// Example:
//
// func TestArrayContains(t *testing.T) {
// numbers := []int{1, 2, 3, 4, 5}
// assert.ArrayContains(3)(numbers)(t) // Passes
// assert.ArrayContains(10)(numbers)(t) // Fails
//
// names := []string{"Alice", "Bob", "Charlie"}
// assert.ArrayContains("Bob")(names)(t) // Passes
// }
func ArrayContains[T any](expected T) Kleisli[[]T] {
return func(actual []T) Reader {
return func(t *testing.T) bool {
@@ -174,7 +374,15 @@ func ArrayContains[T any](expected T) Kleisli[[]T] {
}
}
// ContainsKey tests if a key is contained in a map
// ContainsKey tests if a key is contained in a map.
//
// Example:
//
// func TestContainsKey(t *testing.T) {
// config := map[string]int{"timeout": 30, "retries": 3}
// assert.ContainsKey[int]("timeout")(config)(t) // Passes
// assert.ContainsKey[int]("maxSize")(config)(t) // Fails
// }
func ContainsKey[T any, K comparable](expected K) Kleisli[map[K]T] {
return func(actual map[K]T) Reader {
return func(t *testing.T) bool {
@@ -183,7 +391,15 @@ func ContainsKey[T any, K comparable](expected K) Kleisli[map[K]T] {
}
}
// NotContainsKey tests if a key is not contained in a map
// NotContainsKey tests if a key is not contained in a map.
//
// Example:
//
// func TestNotContainsKey(t *testing.T) {
// config := map[string]int{"timeout": 30, "retries": 3}
// assert.NotContainsKey[int]("maxSize")(config)(t) // Passes
// assert.NotContainsKey[int]("timeout")(config)(t) // Fails
// }
func NotContainsKey[T any, K comparable](expected K) Kleisli[map[K]T] {
return func(actual map[K]T) Reader {
return func(t *testing.T) bool {
@@ -192,7 +408,31 @@ func NotContainsKey[T any, K comparable](expected K) Kleisli[map[K]T] {
}
}
// That asserts that a particular predicate matches
// That asserts that a particular predicate matches.
//
// This is a powerful function that allows you to create custom assertions using predicates.
//
// Example:
//
// func TestThat(t *testing.T) {
// // Test if a number is positive
// isPositive := func(n int) bool { return n > 0 }
// assert.That(isPositive)(42)(t) // Passes
// assert.That(isPositive)(-5)(t) // Fails
//
// // Test if a string is uppercase
// isUppercase := func(s string) bool { return s == strings.ToUpper(s) }
// assert.That(isUppercase)("HELLO")(t) // Passes
// assert.That(isUppercase)("Hello")(t) // Fails
//
// // Can be combined with Local for property testing
// type User struct { Age int }
// ageIsAdult := assert.Local(func(u User) int { return u.Age })(
// assert.That(func(age int) bool { return age >= 18 }),
// )
// user := User{Age: 25}
// ageIsAdult(user)(t) // Passes
// }
func That[T any](pred Predicate[T]) Kleisli[T] {
return func(a T) Reader {
return func(t *testing.T) bool {

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

@@ -0,0 +1,237 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package assert_test
import (
"errors"
"strings"
"testing"
"github.com/IBM/fp-go/v2/assert"
"github.com/IBM/fp-go/v2/result"
)
// Example_basicAssertions demonstrates basic equality and inequality assertions
func Example_basicAssertions() {
// This would be in a real test function
var t *testing.T // placeholder for example
// Basic equality
value := 42
assert.Equal(42)(value)(t)
// String equality
name := "Alice"
assert.Equal("Alice")(name)(t)
// Inequality
assert.NotEqual(10)(value)(t)
}
// Example_arrayAssertions demonstrates array-related assertions
func Example_arrayAssertions() {
var t *testing.T // placeholder for example
numbers := []int{1, 2, 3, 4, 5}
// Check array is not empty
assert.ArrayNotEmpty(numbers)(t)
// Check array length
assert.ArrayLength[int](5)(numbers)(t)
// Check array contains a value
assert.ArrayContains(3)(numbers)(t)
}
// Example_mapAssertions demonstrates map-related assertions
func Example_mapAssertions() {
var t *testing.T // placeholder for example
config := map[string]int{
"timeout": 30,
"retries": 3,
"maxSize": 1000,
}
// Check map is not empty
assert.RecordNotEmpty(config)(t)
// Check map length
assert.RecordLength[string, int](3)(config)(t)
// Check map contains key
assert.ContainsKey[int]("timeout")(config)(t)
// Check map does not contain key
assert.NotContainsKey[int]("unknown")(config)(t)
}
// Example_errorAssertions demonstrates error-related assertions
func Example_errorAssertions() {
var t *testing.T // placeholder for example
// Assert no error
err := doSomethingSuccessful()
assert.NoError(err)(t)
// Assert error exists
err2 := doSomethingThatFails()
assert.Error(err2)(t)
}
// Example_resultAssertions demonstrates Result type assertions
func Example_resultAssertions() {
var t *testing.T // placeholder for example
// Assert success
successResult := result.Of[int](42)
assert.Success(successResult)(t)
// Assert failure
failureResult := result.Left[int](errors.New("something went wrong"))
assert.Failure(failureResult)(t)
}
// Example_predicateAssertions demonstrates custom predicate assertions
func Example_predicateAssertions() {
var t *testing.T // placeholder for example
// Test if a number is positive
isPositive := func(n int) bool { return n > 0 }
assert.That(isPositive)(42)(t)
// Test if a string is uppercase
isUppercase := func(s string) bool { return s == strings.ToUpper(s) }
assert.That(isUppercase)("HELLO")(t)
// Test if a number is even
isEven := func(n int) bool { return n%2 == 0 }
assert.That(isEven)(10)(t)
}
// Example_allOf demonstrates combining multiple assertions
func Example_allOf() {
var t *testing.T // placeholder for example
type User struct {
Name string
Age int
Active bool
}
user := User{Name: "Alice", Age: 30, Active: true}
// Combine multiple assertions
assertions := assert.AllOf([]assert.Reader{
assert.Equal("Alice")(user.Name),
assert.Equal(30)(user.Age),
assert.Equal(true)(user.Active),
})
assertions(t)
}
// Example_runAll demonstrates running named test cases
func Example_runAll() {
var t *testing.T // placeholder for example
testcases := map[string]assert.Reader{
"addition": assert.Equal(4)(2 + 2),
"multiplication": assert.Equal(6)(2 * 3),
"subtraction": assert.Equal(1)(3 - 2),
"division": assert.Equal(2)(10 / 5),
}
assert.RunAll(testcases)(t)
}
// Example_local demonstrates focusing assertions on specific properties
func Example_local() {
var t *testing.T // placeholder for example
type User struct {
Name string
Age int
}
// Create an assertion that checks if age is positive
ageIsPositive := assert.That(func(age int) bool { return age > 0 })
// Focus this assertion on the Age field of User
userAgeIsPositive := assert.Local(func(u User) int { return u.Age })(ageIsPositive)
// Now we can test the whole User object
user := User{Name: "Alice", Age: 30}
userAgeIsPositive(user)(t)
}
// Example_composableAssertions demonstrates building complex assertions
func Example_composableAssertions() {
var t *testing.T // placeholder for example
type Config struct {
Host string
Port int
Timeout int
Retries int
}
config := Config{
Host: "localhost",
Port: 8080,
Timeout: 30,
Retries: 3,
}
// Create focused assertions for each field
validHost := assert.Local(func(c Config) string { return c.Host })(
assert.StringNotEmpty,
)
validPort := assert.Local(func(c Config) int { return c.Port })(
assert.That(func(p int) bool { return p > 0 && p < 65536 }),
)
validTimeout := assert.Local(func(c Config) int { return c.Timeout })(
assert.That(func(t int) bool { return t > 0 }),
)
validRetries := assert.Local(func(c Config) int { return c.Retries })(
assert.That(func(r int) bool { return r >= 0 }),
)
// Combine all assertions
validConfig := assert.AllOf([]assert.Reader{
validHost(config),
validPort(config),
validTimeout(config),
validRetries(config),
})
validConfig(t)
}
// Helper functions for examples
func doSomethingSuccessful() error {
return nil
}
func doSomethingThatFails() error {
return errors.New("operation failed")
}
// Made with Bob

View File

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

View File

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

View File

@@ -73,7 +73,7 @@ type (
// It wraps a standard http.Client and provides functional HTTP operations.
client struct {
delegate *http.Client
doIOE func(*http.Request) IOE.IOEither[error, *http.Response]
doIOE IOE.Kleisli[error, *http.Request, *http.Response]
}
)
@@ -158,7 +158,7 @@ func MakeClient(httpClient *http.Client) Client {
// request := MakeGetRequest("https://api.example.com/data")
// fullResp := ReadFullResponse(client)(request)
// result := fullResp(context.Background())()
func ReadFullResponse(client Client) func(Requester) RIOE.ReaderIOResult[H.FullResponse] {
func ReadFullResponse(client Client) RIOE.Kleisli[Requester, H.FullResponse] {
return func(req Requester) RIOE.ReaderIOResult[H.FullResponse] {
return F.Flow3(
client.Do(req),
@@ -195,7 +195,7 @@ func ReadFullResponse(client Client) func(Requester) RIOE.ReaderIOResult[H.FullR
// request := MakeGetRequest("https://api.example.com/data")
// readBytes := ReadAll(client)
// result := readBytes(request)(context.Background())()
func ReadAll(client Client) func(Requester) RIOE.ReaderIOResult[[]byte] {
func ReadAll(client Client) RIOE.Kleisli[Requester, []byte] {
return F.Flow2(
ReadFullResponse(client),
RIOE.Map(H.Body),
@@ -219,7 +219,7 @@ func ReadAll(client Client) func(Requester) RIOE.ReaderIOResult[[]byte] {
// request := MakeGetRequest("https://api.example.com/text")
// readText := ReadText(client)
// result := readText(request)(context.Background())()
func ReadText(client Client) func(Requester) RIOE.ReaderIOResult[string] {
func ReadText(client Client) RIOE.Kleisli[Requester, string] {
return F.Flow2(
ReadAll(client),
RIOE.Map(B.ToString),
@@ -231,7 +231,7 @@ func ReadText(client Client) func(Requester) RIOE.ReaderIOResult[string] {
// Deprecated: Use [ReadJSON] instead. This function is kept for backward compatibility
// but will be removed in a future version. The capitalized version follows Go naming
// conventions for acronyms.
func ReadJson[A any](client Client) func(Requester) RIOE.ReaderIOResult[A] {
func ReadJson[A any](client Client) RIOE.Kleisli[Requester, A] {
return ReadJSON[A](client)
}
@@ -242,7 +242,7 @@ func ReadJson[A any](client Client) func(Requester) RIOE.ReaderIOResult[A] {
// 3. Reads the response body as bytes
//
// This function is used internally by ReadJSON to ensure proper JSON response handling.
func readJSON(client Client) func(Requester) RIOE.ReaderIOResult[[]byte] {
func readJSON(client Client) RIOE.Kleisli[Requester, []byte] {
return F.Flow3(
ReadFullResponse(client),
RIOE.ChainFirstEitherK(F.Flow2(
@@ -278,7 +278,7 @@ func readJSON(client Client) func(Requester) RIOE.ReaderIOResult[[]byte] {
// request := MakeGetRequest("https://api.example.com/user/1")
// readUser := ReadJSON[User](client)
// result := readUser(request)(context.Background())()
func ReadJSON[A any](client Client) func(Requester) RIOE.ReaderIOResult[A] {
func ReadJSON[A any](client Client) RIOE.Kleisli[Requester, A] {
return F.Flow2(
readJSON(client),
RIOE.ChainEitherK(J.Unmarshal[A]),

View File

@@ -1,3 +1,18 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package endomorphism
import (
@@ -5,6 +20,63 @@ import (
S "github.com/IBM/fp-go/v2/semigroup"
)
// FromSemigroup converts a semigroup into a Kleisli arrow for endomorphisms.
//
// This function takes a semigroup and returns a Kleisli arrow that, when given
// a value of type A, produces an endomorphism that concatenates that value with
// other values using the semigroup's Concat operation.
//
// The resulting Kleisli arrow has the signature: func(A) Endomorphism[A]
// When called with a value 'x', it returns an endomorphism that concatenates
// 'x' with its input using the semigroup's binary operation.
//
// # Data Last Principle
//
// FromSemigroup follows the "data last" principle by using function.Bind2of2,
// which binds the second parameter of the semigroup's Concat operation.
// This means that for a semigroup with Concat(a, b), calling FromSemigroup(s)(x)
// creates an endomorphism that computes Concat(input, x), where the input data
// comes first and the bound value 'x' comes last.
//
// For example, with string concatenation:
// - Semigroup.Concat("Hello", "World") = "HelloWorld"
// - FromSemigroup(semigroup)("World") creates: func(input) = Concat(input, "World")
// - Applying it: endomorphism("Hello") = Concat("Hello", "World") = "HelloWorld"
//
// This is particularly useful for creating endomorphisms from associative operations
// like string concatenation, number addition, list concatenation, etc.
//
// Parameters:
// - s: A semigroup providing the Concat operation for type A
//
// Returns:
// - A Kleisli arrow that converts values of type A into endomorphisms
//
// Example:
//
// import (
// "github.com/IBM/fp-go/v2/endomorphism"
// "github.com/IBM/fp-go/v2/semigroup"
// )
//
// // Create a semigroup for integer addition
// addSemigroup := semigroup.MakeSemigroup(func(a, b int) int {
// return a + b
// })
//
// // Convert it to a Kleisli arrow
// addKleisli := endomorphism.FromSemigroup(addSemigroup)
//
// // Use the Kleisli arrow to create an endomorphism that adds 5
// // This follows "data last": the input data comes first, 5 comes last
// addFive := addKleisli(5)
//
// // Apply the endomorphism: Concat(10, 5) = 10 + 5 = 15
// result := addFive(10) // result is 15
//
// The function uses function.Bind2of2 to partially apply the semigroup's Concat
// operation, effectively currying it to create the desired Kleisli arrow while
// maintaining the "data last" principle.
func FromSemigroup[A any](s S.Semigroup[A]) Kleisli[A] {
return function.Bind2of2(s.Concat)
}

View File

@@ -0,0 +1,441 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package endomorphism
import (
"testing"
S "github.com/IBM/fp-go/v2/semigroup"
"github.com/stretchr/testify/assert"
)
// TestFromSemigroup tests the FromSemigroup function with various semigroups
func TestFromSemigroup(t *testing.T) {
t.Run("integer addition semigroup", func(t *testing.T) {
// Create a semigroup for integer addition
addSemigroup := S.MakeSemigroup(func(a, b int) int {
return a + b
})
// Convert to Kleisli arrow
addKleisli := FromSemigroup(addSemigroup)
// Create an endomorphism that adds 5
addFive := addKleisli(5)
// Test the endomorphism
assert.Equal(t, 15, addFive(10), "addFive(10) should equal 15")
assert.Equal(t, 5, addFive(0), "addFive(0) should equal 5")
assert.Equal(t, -5, addFive(-10), "addFive(-10) should equal -5")
})
t.Run("integer multiplication semigroup", func(t *testing.T) {
// Create a semigroup for integer multiplication
mulSemigroup := S.MakeSemigroup(func(a, b int) int {
return a * b
})
// Convert to Kleisli arrow
mulKleisli := FromSemigroup(mulSemigroup)
// Create an endomorphism that multiplies by 3
multiplyByThree := mulKleisli(3)
// Test the endomorphism
assert.Equal(t, 15, multiplyByThree(5), "multiplyByThree(5) should equal 15")
assert.Equal(t, 0, multiplyByThree(0), "multiplyByThree(0) should equal 0")
assert.Equal(t, -9, multiplyByThree(-3), "multiplyByThree(-3) should equal -9")
})
t.Run("string concatenation semigroup", func(t *testing.T) {
// Create a semigroup for string concatenation
concatSemigroup := S.MakeSemigroup(func(a, b string) string {
return a + b
})
// Convert to Kleisli arrow
concatKleisli := FromSemigroup(concatSemigroup)
// Create an endomorphism that appends "Hello, " (input is on the left)
appendHello := concatKleisli("Hello, ")
// Test the endomorphism - input is concatenated on the left, "Hello, " on the right
assert.Equal(t, "WorldHello, ", appendHello("World"), "appendHello('World') should equal 'WorldHello, '")
assert.Equal(t, "Hello, ", appendHello(""), "appendHello('') should equal 'Hello, '")
assert.Equal(t, "GoHello, ", appendHello("Go"), "appendHello('Go') should equal 'GoHello, '")
})
t.Run("slice concatenation semigroup", func(t *testing.T) {
// Create a semigroup for slice concatenation
sliceSemigroup := S.MakeSemigroup(func(a, b []int) []int {
result := make([]int, len(a)+len(b))
copy(result, a)
copy(result[len(a):], b)
return result
})
// Convert to Kleisli arrow
sliceKleisli := FromSemigroup(sliceSemigroup)
// Create an endomorphism that appends [1, 2] (input is on the left)
appendOneTwo := sliceKleisli([]int{1, 2})
// Test the endomorphism - input is concatenated on the left, [1,2] on the right
result1 := appendOneTwo([]int{3, 4, 5})
assert.Equal(t, []int{3, 4, 5, 1, 2}, result1, "appendOneTwo([3,4,5]) should equal [3,4,5,1,2]")
result2 := appendOneTwo([]int{})
assert.Equal(t, []int{1, 2}, result2, "appendOneTwo([]) should equal [1,2]")
result3 := appendOneTwo([]int{10})
assert.Equal(t, []int{10, 1, 2}, result3, "appendOneTwo([10]) should equal [10,1,2]")
})
t.Run("max semigroup", func(t *testing.T) {
// Create a semigroup for max operation
maxSemigroup := S.MakeSemigroup(func(a, b int) int {
if a > b {
return a
}
return b
})
// Convert to Kleisli arrow
maxKleisli := FromSemigroup(maxSemigroup)
// Create an endomorphism that takes max with 10
maxWithTen := maxKleisli(10)
// Test the endomorphism
assert.Equal(t, 15, maxWithTen(15), "maxWithTen(15) should equal 15")
assert.Equal(t, 10, maxWithTen(5), "maxWithTen(5) should equal 10")
assert.Equal(t, 10, maxWithTen(10), "maxWithTen(10) should equal 10")
assert.Equal(t, 10, maxWithTen(-5), "maxWithTen(-5) should equal 10")
})
t.Run("min semigroup", func(t *testing.T) {
// Create a semigroup for min operation
minSemigroup := S.MakeSemigroup(func(a, b int) int {
if a < b {
return a
}
return b
})
// Convert to Kleisli arrow
minKleisli := FromSemigroup(minSemigroup)
// Create an endomorphism that takes min with 10
minWithTen := minKleisli(10)
// Test the endomorphism
assert.Equal(t, 5, minWithTen(5), "minWithTen(5) should equal 5")
assert.Equal(t, 10, minWithTen(15), "minWithTen(15) should equal 10")
assert.Equal(t, 10, minWithTen(10), "minWithTen(10) should equal 10")
assert.Equal(t, -5, minWithTen(-5), "minWithTen(-5) should equal -5")
})
}
// TestFromSemigroupComposition tests that endomorphisms created from semigroups can be composed
func TestFromSemigroupComposition(t *testing.T) {
t.Run("compose addition endomorphisms", func(t *testing.T) {
// Create a semigroup for integer addition
addSemigroup := S.MakeSemigroup(func(a, b int) int {
return a + b
})
addKleisli := FromSemigroup(addSemigroup)
// Create two endomorphisms
addFive := addKleisli(5)
addTen := addKleisli(10)
// Compose them (RIGHT-TO-LEFT execution)
composed := MonadCompose(addFive, addTen)
// Test composition: addTen first, then addFive
result := composed(3) // 3 + 10 = 13, then 13 + 5 = 18
assert.Equal(t, 18, result, "composed addition should work correctly")
})
t.Run("compose string endomorphisms", func(t *testing.T) {
// Create a semigroup for string concatenation
concatSemigroup := S.MakeSemigroup(func(a, b string) string {
return a + b
})
concatKleisli := FromSemigroup(concatSemigroup)
// Create two endomorphisms
appendHello := concatKleisli("Hello, ")
appendExclamation := concatKleisli("!")
// Compose them (RIGHT-TO-LEFT execution)
composed := MonadCompose(appendHello, appendExclamation)
// Test composition: appendExclamation first, then appendHello
// "World" + "!" = "World!", then "World!" + "Hello, " = "World!Hello, "
result := composed("World")
assert.Equal(t, "World!Hello, ", result, "composed string operations should work correctly")
})
}
// TestFromSemigroupWithMonoid tests using FromSemigroup-created endomorphisms with monoid operations
func TestFromSemigroupWithMonoid(t *testing.T) {
t.Run("monoid concat with addition endomorphisms", func(t *testing.T) {
// Create a semigroup for integer addition
addSemigroup := S.MakeSemigroup(func(a, b int) int {
return a + b
})
addKleisli := FromSemigroup(addSemigroup)
// Create multiple endomorphisms
addOne := addKleisli(1)
addTwo := addKleisli(2)
addThree := addKleisli(3)
// Use monoid to combine them
monoid := Monoid[int]()
combined := monoid.Concat(monoid.Concat(addOne, addTwo), addThree)
// Test: RIGHT-TO-LEFT execution: addThree, then addTwo, then addOne
result := combined(10) // 10 + 3 = 13, 13 + 2 = 15, 15 + 1 = 16
assert.Equal(t, 16, result, "monoid combination should work correctly")
})
}
// TestFromSemigroupAssociativity tests that the semigroup associativity is preserved
func TestFromSemigroupAssociativity(t *testing.T) {
t.Run("addition associativity", func(t *testing.T) {
// Create a semigroup for integer addition
addSemigroup := S.MakeSemigroup(func(a, b int) int {
return a + b
})
addKleisli := FromSemigroup(addSemigroup)
// Create three endomorphisms
addTwo := addKleisli(2)
addThree := addKleisli(3)
addFive := addKleisli(5)
// Test associativity: (a . b) . c = a . (b . c)
left := MonadCompose(MonadCompose(addTwo, addThree), addFive)
right := MonadCompose(addTwo, MonadCompose(addThree, addFive))
testValue := 10
assert.Equal(t, left(testValue), right(testValue), "composition should be associative")
// Both should equal: 10 + 5 + 3 + 2 = 20
assert.Equal(t, 20, left(testValue), "left composition should equal 20")
assert.Equal(t, 20, right(testValue), "right composition should equal 20")
})
t.Run("string concatenation associativity", func(t *testing.T) {
// Create a semigroup for string concatenation
concatSemigroup := S.MakeSemigroup(func(a, b string) string {
return a + b
})
concatKleisli := FromSemigroup(concatSemigroup)
// Create three endomorphisms
appendA := concatKleisli("A")
appendB := concatKleisli("B")
appendC := concatKleisli("C")
// Test associativity: (a . b) . c = a . (b . c)
left := MonadCompose(MonadCompose(appendA, appendB), appendC)
right := MonadCompose(appendA, MonadCompose(appendB, appendC))
testValue := "X"
assert.Equal(t, left(testValue), right(testValue), "string composition should be associative")
// Both should equal: "X" + "C" + "B" + "A" = "XCBA" (RIGHT-TO-LEFT composition)
assert.Equal(t, "XCBA", left(testValue), "left composition should equal 'XCBA'")
assert.Equal(t, "XCBA", right(testValue), "right composition should equal 'XCBA'")
})
}
// TestFromSemigroupEdgeCases tests edge cases and boundary conditions
func TestFromSemigroupEdgeCases(t *testing.T) {
t.Run("zero values", func(t *testing.T) {
// Test with addition and zero
addSemigroup := S.MakeSemigroup(func(a, b int) int {
return a + b
})
addKleisli := FromSemigroup(addSemigroup)
addZero := addKleisli(0)
assert.Equal(t, 5, addZero(5), "adding zero should not change the value")
assert.Equal(t, 0, addZero(0), "adding zero to zero should be zero")
assert.Equal(t, -3, addZero(-3), "adding zero to negative should not change")
})
t.Run("empty string", func(t *testing.T) {
// Test with string concatenation and empty string
concatSemigroup := S.MakeSemigroup(func(a, b string) string {
return a + b
})
concatKleisli := FromSemigroup(concatSemigroup)
prependEmpty := concatKleisli("")
assert.Equal(t, "hello", prependEmpty("hello"), "prepending empty string should not change")
assert.Equal(t, "", prependEmpty(""), "prepending empty to empty should be empty")
})
t.Run("empty slice", func(t *testing.T) {
// Test with slice concatenation and empty slice
sliceSemigroup := S.MakeSemigroup(func(a, b []int) []int {
result := make([]int, len(a)+len(b))
copy(result, a)
copy(result[len(a):], b)
return result
})
sliceKleisli := FromSemigroup(sliceSemigroup)
prependEmpty := sliceKleisli([]int{})
result := prependEmpty([]int{1, 2, 3})
assert.Equal(t, []int{1, 2, 3}, result, "prepending empty slice should not change")
emptyResult := prependEmpty([]int{})
assert.Equal(t, []int{}, emptyResult, "prepending empty to empty should be empty")
})
}
// TestFromSemigroupDataLastPrinciple explicitly tests that FromSemigroup follows the "data last" principle
func TestFromSemigroupDataLastPrinciple(t *testing.T) {
t.Run("data last with string concatenation", func(t *testing.T) {
// Create a semigroup for string concatenation
// Concat(a, b) = a + b
concatSemigroup := S.MakeSemigroup(func(a, b string) string {
return a + b
})
// FromSemigroup uses Bind2of2, which binds the second parameter
// So FromSemigroup(s)(x) creates: func(input) = Concat(input, x)
// This is "data last" - the input data comes first, bound value comes last
kleisli := FromSemigroup(concatSemigroup)
// Bind "World" as the second parameter
appendWorld := kleisli("World")
// When we call appendWorld("Hello"), it computes Concat("Hello", "World")
// The input "Hello" is the first parameter (data), "World" is the second (bound value)
result := appendWorld("Hello")
assert.Equal(t, "HelloWorld", result, "Data last: Concat(input='Hello', bound='World') = 'HelloWorld'")
// Verify with different input
result2 := appendWorld("Goodbye")
assert.Equal(t, "GoodbyeWorld", result2, "Data last: Concat(input='Goodbye', bound='World') = 'GoodbyeWorld'")
})
t.Run("data last with integer addition", func(t *testing.T) {
// Create a semigroup for integer addition
// Concat(a, b) = a + b
addSemigroup := S.MakeSemigroup(func(a, b int) int {
return a + b
})
// FromSemigroup binds the second parameter
// So FromSemigroup(s)(5) creates: func(input) = Concat(input, 5) = input + 5
kleisli := FromSemigroup(addSemigroup)
// Bind 5 as the second parameter
addFive := kleisli(5)
// When we call addFive(10), it computes Concat(10, 5) = 10 + 5 = 15
// The input 10 is the first parameter (data), 5 is the second (bound value)
result := addFive(10)
assert.Equal(t, 15, result, "Data last: Concat(input=10, bound=5) = 15")
})
t.Run("data last with non-commutative operation", func(t *testing.T) {
// Create a semigroup for a non-commutative operation to clearly show order
// Concat(a, b) = a - b (subtraction is not commutative)
subSemigroup := S.MakeSemigroup(func(a, b int) int {
return a - b
})
// FromSemigroup binds the second parameter
// So FromSemigroup(s)(5) creates: func(input) = Concat(input, 5) = input - 5
kleisli := FromSemigroup(subSemigroup)
// Bind 5 as the second parameter
subtractFive := kleisli(5)
// When we call subtractFive(10), it computes Concat(10, 5) = 10 - 5 = 5
// The input 10 is the first parameter (data), 5 is the second (bound value)
result := subtractFive(10)
assert.Equal(t, 5, result, "Data last: Concat(input=10, bound=5) = 10 - 5 = 5")
// If it were "data first" (binding first parameter), we would get:
// Concat(5, 10) = 5 - 10 = -5, which is NOT what we get
assert.NotEqual(t, -5, result, "Not data first: result is NOT Concat(bound=5, input=10) = 5 - 10 = -5")
})
t.Run("data last with list concatenation", func(t *testing.T) {
// Create a semigroup for list concatenation
// Concat(a, b) = a ++ b
listSemigroup := S.MakeSemigroup(func(a, b []int) []int {
result := make([]int, len(a)+len(b))
copy(result, a)
copy(result[len(a):], b)
return result
})
// FromSemigroup binds the second parameter
// So FromSemigroup(s)([3,4]) creates: func(input) = Concat(input, [3,4])
kleisli := FromSemigroup(listSemigroup)
// Bind [3, 4] as the second parameter
appendThreeFour := kleisli([]int{3, 4})
// When we call appendThreeFour([1,2]), it computes Concat([1,2], [3,4]) = [1,2,3,4]
// The input [1,2] is the first parameter (data), [3,4] is the second (bound value)
result := appendThreeFour([]int{1, 2})
assert.Equal(t, []int{1, 2, 3, 4}, result, "Data last: Concat(input=[1,2], bound=[3,4]) = [1,2,3,4]")
})
}
// BenchmarkFromSemigroup benchmarks the FromSemigroup function
func BenchmarkFromSemigroup(b *testing.B) {
addSemigroup := S.MakeSemigroup(func(a, b int) int {
return a + b
})
addKleisli := FromSemigroup(addSemigroup)
addFive := addKleisli(5)
b.ResetTimer()
for b.Loop() {
_ = addFive(10)
}
}
// BenchmarkFromSemigroupComposition benchmarks composed endomorphisms from semigroups
func BenchmarkFromSemigroupComposition(b *testing.B) {
addSemigroup := S.MakeSemigroup(func(a, b int) int {
return a + b
})
addKleisli := FromSemigroup(addSemigroup)
addFive := addKleisli(5)
addTen := addKleisli(10)
composed := MonadCompose(addFive, addTen)
b.ResetTimer()
for b.Loop() {
_ = composed(3)
}
}
// Made with Bob

View File

@@ -36,7 +36,7 @@ package function
// Example:
//
// isPositive := func(n int) bool { return n > 0 }
// double := func(n int) int { return n * 2 }
// double := N.Mul(2)
// negate := func(n int) int { return -n }
//
// transform := Ternary(isPositive, double, negate)

View File

@@ -107,8 +107,8 @@ Chain for sequential composition:
// Chain multiple operations
result := F.Pipe2(
10,
identity.Chain(func(n int) int { return n * 2 }),
identity.Chain(func(n int) int { return n + 5 }),
identity.Chain(N.Mul(2)),
identity.Chain(N.Add(5)),
)
// result is 25
@@ -177,8 +177,8 @@ Convert tuples of Identity values:
// Traverse with transformation
tuple := T.MakeTuple2(1, 2)
result := identity.TraverseTuple2(
func(n int) int { return n * 2 },
func(n int) int { return n * 3 },
N.Mul(2),
N.Mul(3),
)(tuple)
// result is T.Tuple2[int, int]{2, 6}
@@ -211,7 +211,7 @@ Example of generic code:
) M {
return F.Pipe2(
monad.Of(value),
monad.Map(func(n int) int { return n * 2 }),
monad.Map(N.Mul(2)),
monad.Map(func(n int) string { return fmt.Sprintf("%d", n) }),
)
}

View File

@@ -17,10 +17,13 @@ package identity
import (
"fmt"
"strconv"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
N "github.com/IBM/fp-go/v2/number"
S "github.com/IBM/fp-go/v2/string"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/stretchr/testify/assert"
)
@@ -51,17 +54,15 @@ func TestMap(t *testing.T) {
})
t.Run("transforms string", func(t *testing.T) {
result := F.Pipe1("hello", Map(func(s string) int {
return len(s)
}))
result := F.Pipe1("hello", Map(S.Size))
assert.Equal(t, 5, result)
})
t.Run("chains multiple maps", func(t *testing.T) {
result := F.Pipe2(
5,
Map(func(n int) int { return n * 2 }),
Map(func(n int) int { return n + 3 }),
Map(N.Mul(2)),
Map(N.Add(3)),
)
assert.Equal(t, 13, result)
})
@@ -69,14 +70,12 @@ func TestMap(t *testing.T) {
func TestMonadMap(t *testing.T) {
t.Run("transforms value", func(t *testing.T) {
result := MonadMap(10, func(n int) int { return n * 3 })
result := MonadMap(10, N.Mul(3))
assert.Equal(t, 30, result)
})
t.Run("changes type", func(t *testing.T) {
result := MonadMap(42, func(n int) string {
return fmt.Sprintf("Number: %d", n)
})
result := MonadMap(42, S.Format[int]("Number: %d"))
assert.Equal(t, "Number: 42", result)
})
}
@@ -109,23 +108,21 @@ func TestChain(t *testing.T) {
t.Run("chains multiple operations", func(t *testing.T) {
result := F.Pipe2(
10,
Chain(func(n int) int { return n * 2 }),
Chain(func(n int) int { return n + 5 }),
Chain(N.Mul(2)),
Chain(N.Add(5)),
)
assert.Equal(t, 25, result)
})
t.Run("changes type", func(t *testing.T) {
result := F.Pipe1(5, Chain(func(n int) string {
return fmt.Sprintf("Value: %d", n)
}))
result := F.Pipe1(5, Chain(S.Format[int]("Value: %d")))
assert.Equal(t, "Value: 5", result)
})
}
func TestMonadChain(t *testing.T) {
t.Run("chains computation", func(t *testing.T) {
result := MonadChain(7, func(n int) int { return n * 7 })
result := MonadChain(7, N.Mul(7))
assert.Equal(t, 49, result)
})
}
@@ -148,7 +145,7 @@ func TestChainFirst(t *testing.T) {
result := F.Pipe2(
10,
ChainFirst(func(n int) string { return "ignored" }),
Map(func(n int) int { return n * 2 }),
Map(N.Mul(2)),
)
assert.Equal(t, 20, result)
})
@@ -156,9 +153,7 @@ func TestChainFirst(t *testing.T) {
func TestMonadChainFirst(t *testing.T) {
t.Run("keeps original value", func(t *testing.T) {
result := MonadChainFirst(100, func(n int) string {
return fmt.Sprintf("%d", n)
})
result := MonadChainFirst(100, strconv.Itoa)
assert.Equal(t, 100, result)
})
}
@@ -170,17 +165,13 @@ func TestAp(t *testing.T) {
})
t.Run("applies curried function", func(t *testing.T) {
add := func(a int) func(int) int {
return func(b int) int { return a + b }
}
add := N.Add[int]
result := F.Pipe1(add(10), Ap[int](5))
assert.Equal(t, 15, result)
})
t.Run("changes type", func(t *testing.T) {
toString := func(n int) string {
return fmt.Sprintf("Number: %d", n)
}
toString := S.Format[int]("Number: %d")
result := F.Pipe1(toString, Ap[string](42))
assert.Equal(t, "Number: 42", result)
})
@@ -188,22 +179,22 @@ func TestAp(t *testing.T) {
func TestMonadAp(t *testing.T) {
t.Run("applies function to value", func(t *testing.T) {
result := MonadAp(func(n int) int { return n * 3 }, 7)
result := MonadAp(N.Mul(3), 7)
assert.Equal(t, 21, result)
})
}
func TestFlap(t *testing.T) {
t.Run("flips application", func(t *testing.T) {
double := func(n int) int { return n * 2 }
double := N.Mul(2)
result := F.Pipe1(double, Flap[int](5))
assert.Equal(t, 10, result)
})
t.Run("with multiple functions", func(t *testing.T) {
funcs := []func(int) int{
func(n int) int { return n * 2 },
func(n int) int { return n + 10 },
N.Mul(2),
N.Add(10),
func(n int) int { return n * n },
}
@@ -218,9 +209,7 @@ func TestFlap(t *testing.T) {
func TestMonadFlap(t *testing.T) {
t.Run("applies value to function", func(t *testing.T) {
result := MonadFlap(func(n int) string {
return fmt.Sprintf("Value: %d", n)
}, 42)
result := MonadFlap(S.Format[int]("Value: %d"), 42)
assert.Equal(t, "Value: 42", result)
})
}
@@ -391,8 +380,8 @@ func TestTraverseTuple(t *testing.T) {
t.Run("TraverseTuple2", func(t *testing.T) {
tuple := T.MakeTuple2(1, 2)
result := TraverseTuple2(
func(n int) int { return n * 2 },
func(n int) int { return n * 3 },
N.Mul(2),
N.Mul(3),
)(tuple)
assert.Equal(t, T.MakeTuple2(2, 6), result)
})
@@ -400,7 +389,7 @@ func TestTraverseTuple(t *testing.T) {
t.Run("TraverseTuple3", func(t *testing.T) {
tuple := T.MakeTuple3(1, 2, 3)
result := TraverseTuple3(
func(n int) int { return n + 10 },
N.Add(10),
func(n int) int { return n + 20 },
func(n int) int { return n + 30 },
)(tuple)
@@ -426,15 +415,11 @@ func TestMonad(t *testing.T) {
assert.Equal(t, 42, value)
// Test Map
mapped := m.Map(func(n int) string {
return fmt.Sprintf("Number: %d", n)
})(value)
mapped := m.Map(S.Format[int]("Number: %d"))(value)
assert.Equal(t, "Number: 42", mapped)
// Test Chain
chained := m.Chain(func(n int) string {
return fmt.Sprintf("Value: %d", n)
})(value)
chained := m.Chain(S.Format[int]("Value: %d"))(value)
assert.Equal(t, "Value: 42", chained)
// Test Ap
@@ -450,7 +435,7 @@ func TestMonadLaws(t *testing.T) {
t.Run("left identity", func(t *testing.T) {
// Of(a).Chain(f) === f(a)
a := 42
f := func(n int) int { return n * 2 }
f := N.Mul(2)
left := F.Pipe1(Of(a), Chain(f))
right := f(a)
@@ -470,8 +455,8 @@ func TestMonadLaws(t *testing.T) {
t.Run("associativity", func(t *testing.T) {
// m.Chain(f).Chain(g) === m.Chain(x => f(x).Chain(g))
m := 5
f := func(n int) int { return n * 2 }
g := func(n int) int { return n + 10 }
f := N.Mul(2)
g := N.Add(10)
left := F.Pipe2(m, Chain(f), Chain(g))
right := F.Pipe1(m, Chain(func(x int) int {
@@ -496,8 +481,8 @@ func TestFunctorLaws(t *testing.T) {
t.Run("composition", func(t *testing.T) {
// Map(f).Map(g) === Map(g ∘ f)
value := 5
f := func(n int) int { return n * 2 }
g := func(n int) int { return n + 10 }
f := N.Mul(2)
g := N.Add(10)
left := F.Pipe2(value, Map(f), Map(g))
right := F.Pipe1(value, Map(F.Flow2(f, g)))
@@ -541,7 +526,7 @@ func TestTraverseTuple4(t *testing.T) {
t.Run("traverses tuple4", func(t *testing.T) {
tuple := T.MakeTuple4(1, 2, 3, 4)
result := TraverseTuple4(
func(n int) int { return n + 10 },
N.Add(10),
func(n int) int { return n + 20 },
func(n int) int { return n + 30 },
func(n int) int { return n + 40 },
@@ -570,8 +555,8 @@ func TestTraverseTuple5(t *testing.T) {
tuple := T.MakeTuple5(1, 2, 3, 4, 5)
result := TraverseTuple5(
func(n int) int { return n * 1 },
func(n int) int { return n * 2 },
func(n int) int { return n * 3 },
N.Mul(2),
N.Mul(3),
func(n int) int { return n * 4 },
func(n int) int { return n * 5 },
)(tuple)
@@ -598,11 +583,11 @@ func TestTraverseTuple6(t *testing.T) {
t.Run("traverses tuple6", func(t *testing.T) {
tuple := T.MakeTuple6(1, 2, 3, 4, 5, 6)
result := TraverseTuple6(
func(n int) int { return n + 1 },
N.Add(1),
func(n int) int { return n + 2 },
func(n int) int { return n + 3 },
N.Add(3),
func(n int) int { return n + 4 },
func(n int) int { return n + 5 },
N.Add(5),
func(n int) int { return n + 6 },
)(tuple)
assert.Equal(t, T.MakeTuple6(2, 4, 6, 8, 10, 12), result)
@@ -691,15 +676,15 @@ func TestTraverseTuple9(t *testing.T) {
t.Run("traverses tuple9", func(t *testing.T) {
tuple := T.MakeTuple9(1, 2, 3, 4, 5, 6, 7, 8, 9)
result := TraverseTuple9(
func(n int) int { return n + 1 },
func(n int) int { return n + 1 },
func(n int) int { return n + 1 },
func(n int) int { return n + 1 },
func(n int) int { return n + 1 },
func(n int) int { return n + 1 },
func(n int) int { return n + 1 },
func(n int) int { return n + 1 },
func(n int) int { return n + 1 },
N.Add(1),
N.Add(1),
N.Add(1),
N.Add(1),
N.Add(1),
N.Add(1),
N.Add(1),
N.Add(1),
N.Add(1),
)(tuple)
assert.Equal(t, T.MakeTuple9(2, 3, 4, 5, 6, 7, 8, 9, 10), result)
})
@@ -724,16 +709,16 @@ func TestTraverseTuple10(t *testing.T) {
t.Run("traverses tuple10", func(t *testing.T) {
tuple := T.MakeTuple10(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
result := TraverseTuple10(
func(n int) int { return n * 2 },
func(n int) int { return n * 2 },
func(n int) int { return n * 2 },
func(n int) int { return n * 2 },
func(n int) int { return n * 2 },
func(n int) int { return n * 2 },
func(n int) int { return n * 2 },
func(n int) int { return n * 2 },
func(n int) int { return n * 2 },
func(n int) int { return n * 2 },
N.Mul(2),
N.Mul(2),
N.Mul(2),
N.Mul(2),
N.Mul(2),
N.Mul(2),
N.Mul(2),
N.Mul(2),
N.Mul(2),
N.Mul(2),
)(tuple)
assert.Equal(t, T.MakeTuple10(2, 4, 6, 8, 10, 12, 14, 16, 18, 20), result)
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,6 +22,7 @@ import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
G "github.com/IBM/fp-go/v2/io"
N "github.com/IBM/fp-go/v2/number"
"github.com/stretchr/testify/assert"
)
@@ -62,7 +63,7 @@ func TestOf(t *testing.T) {
func TestMonadMap(t *testing.T) {
rio := Of[ReaderTestConfig](5)
doubled := MonadMap(rio, func(n int) int { return n * 2 })
doubled := MonadMap(rio, N.Mul(2))
config := ReaderTestConfig{Value: 1, Name: "test"}
result := doubled(config)()
@@ -102,7 +103,7 @@ func TestChain(t *testing.T) {
}
func TestMonadAp(t *testing.T) {
fabIO := Of[ReaderTestConfig](func(n int) int { return n * 2 })
fabIO := Of[ReaderTestConfig](N.Mul(2))
faIO := Of[ReaderTestConfig](5)
result := MonadAp(fabIO, faIO)
@@ -120,7 +121,7 @@ func TestAp(t *testing.T) {
}
func TestMonadApSeq(t *testing.T) {
fabIO := Of[ReaderTestConfig](func(n int) int { return n + 10 })
fabIO := Of[ReaderTestConfig](N.Add(10))
faIO := Of[ReaderTestConfig](5)
result := MonadApSeq(fabIO, faIO)
@@ -129,7 +130,7 @@ func TestMonadApSeq(t *testing.T) {
}
func TestMonadApPar(t *testing.T) {
fabIO := Of[ReaderTestConfig](func(n int) int { return n + 10 })
fabIO := Of[ReaderTestConfig](N.Add(10))
faIO := Of[ReaderTestConfig](5)
result := MonadApPar(fabIO, faIO)
@@ -238,7 +239,7 @@ func TestFlatten(t *testing.T) {
}
func TestMonadFlap(t *testing.T) {
fabIO := Of[ReaderTestConfig](func(n int) int { return n * 3 })
fabIO := Of[ReaderTestConfig](N.Mul(3))
result := MonadFlap(fabIO, 7)
config := ReaderTestConfig{Value: 1, Name: "test"}
@@ -247,7 +248,7 @@ func TestMonadFlap(t *testing.T) {
func TestFlap(t *testing.T) {
result := F.Pipe1(
Of[ReaderTestConfig](func(n int) int { return n * 3 }),
Of[ReaderTestConfig](N.Mul(3)),
Flap[ReaderTestConfig, int, int](7),
)
@@ -263,7 +264,7 @@ func TestComplexPipeline(t *testing.T) {
Chain(func(n int) ReaderIO[ReaderTestConfig, int] {
return Of[ReaderTestConfig](n * 2)
}),
Map[ReaderTestConfig](func(n int) int { return n + 10 }),
Map[ReaderTestConfig](N.Add(10)),
)
config := ReaderTestConfig{Value: 5, Name: "test"}
@@ -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() {})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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