From 24c0519cc744b7a1bcc73e0110d7560becff1d7c Mon Sep 17 00:00:00 2001 From: "Dr. Carsten Leue" Date: Thu, 4 Dec 2025 16:31:21 +0100 Subject: [PATCH] fix: try to unify type signatures Signed-off-by: Dr. Carsten Leue --- v2/DESIGN.md | 574 ++++++++++++++++++ v2/assert/assert.go | 272 ++++++++- v2/assert/example_test.go | 237 ++++++++ v2/context/readerio/reader_test.go | 21 +- v2/context/readerio/type.go | 16 + v2/context/readerioresult/http/request.go | 14 +- v2/endomorphism/from.go | 72 +++ v2/endomorphism/from_test.go | 441 ++++++++++++++ v2/function/ternary.go | 2 +- v2/identity/doc.go | 10 +- v2/identity/identity_test.go | 127 ++-- v2/io/io.go | 6 +- v2/optics/README.md | 2 +- v2/optics/lens/optional/optional.go | 2 +- v2/optics/optional/optional.go | 62 +- v2/optics/prism/prism.go | 17 +- v2/optics/prism/prisms.go | 5 + v2/optics/prism/types.go | 23 + v2/optics/traversal/doc.go | 8 +- v2/pair/doc.go | 2 +- v2/readerio/reader.go | 12 +- v2/readerio/reader_test.go | 17 +- v2/readeroption/array.go | 2 +- v2/readeroption/bind.go | 18 +- v2/readeroption/reader.go | 50 +- v2/readeroption/reader_test.go | 2 +- v2/readeroption/types.go | 3 + v2/samples/http/http_test.go | 6 +- v2/samples/match/examples_match_test.go | 10 +- .../chapter08_tupperware_test.go | 21 +- .../chapter09_monadiconions_test.go | 4 +- v2/samples/mostly-adequate/types.go | 61 +- v2/tuple/tuple_test.go | 219 +++---- 33 files changed, 2025 insertions(+), 313 deletions(-) create mode 100644 v2/DESIGN.md create mode 100644 v2/assert/example_test.go create mode 100644 v2/endomorphism/from_test.go diff --git a/v2/DESIGN.md b/v2/DESIGN.md new file mode 100644 index 0000000..e673868 --- /dev/null +++ b/v2/DESIGN.md @@ -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 \ No newline at end of file diff --git a/v2/assert/assert.go b/v2/assert/assert.go index 28897c5..d592eaf 100644 --- a/v2/assert/assert.go +++ b/v2/assert/assert.go @@ -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 { diff --git a/v2/assert/example_test.go b/v2/assert/example_test.go new file mode 100644 index 0000000..0b5ec7a --- /dev/null +++ b/v2/assert/example_test.go @@ -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 diff --git a/v2/context/readerio/reader_test.go b/v2/context/readerio/reader_test.go index 069236d..cf0cb1b 100644 --- a/v2/context/readerio/reader_test.go +++ b/v2/context/readerio/reader_test.go @@ -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() {}) diff --git a/v2/context/readerio/type.go b/v2/context/readerio/type.go index 045a7f7..5cd46c0 100644 --- a/v2/context/readerio/type.go +++ b/v2/context/readerio/type.go @@ -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] ) diff --git a/v2/context/readerioresult/http/request.go b/v2/context/readerioresult/http/request.go index d2a009f..a285cf4 100644 --- a/v2/context/readerioresult/http/request.go +++ b/v2/context/readerioresult/http/request.go @@ -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]), diff --git a/v2/endomorphism/from.go b/v2/endomorphism/from.go index ad6f0b1..7da4aef 100644 --- a/v2/endomorphism/from.go +++ b/v2/endomorphism/from.go @@ -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) } diff --git a/v2/endomorphism/from_test.go b/v2/endomorphism/from_test.go new file mode 100644 index 0000000..6b2a975 --- /dev/null +++ b/v2/endomorphism/from_test.go @@ -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 diff --git a/v2/function/ternary.go b/v2/function/ternary.go index ed99a8c..c3784a4 100644 --- a/v2/function/ternary.go +++ b/v2/function/ternary.go @@ -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) diff --git a/v2/identity/doc.go b/v2/identity/doc.go index 060cd32..4d23cf8 100644 --- a/v2/identity/doc.go +++ b/v2/identity/doc.go @@ -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) }), ) } diff --git a/v2/identity/identity_test.go b/v2/identity/identity_test.go index e6d745b..873433d 100644 --- a/v2/identity/identity_test.go +++ b/v2/identity/identity_test.go @@ -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) }) diff --git a/v2/io/io.go b/v2/io/io.go index 1101304..1109091 100644 --- a/v2/io/io.go +++ b/v2/io/io.go @@ -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) diff --git a/v2/optics/README.md b/v2/optics/README.md index ba6e343..c7179a0 100644 --- a/v2/optics/README.md +++ b/v2/optics/README.md @@ -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] ``` diff --git a/v2/optics/lens/optional/optional.go b/v2/optics/lens/optional/optional.go index 2c96d14..6af87c4 100644 --- a/v2/optics/lens/optional/optional.go +++ b/v2/optics/lens/optional/optional.go @@ -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) }) diff --git a/v2/optics/optional/optional.go b/v2/optics/optional/optional.go index 3865769..18cc808 100644 --- a/v2/optics/optional/optional.go +++ b/v2/optics/optional/optional.go @@ -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] { diff --git a/v2/optics/prism/prism.go b/v2/optics/prism/prism.go index 1781676..939d1ce 100644 --- a/v2/optics/prism/prism.go +++ b/v2/optics/prism/prism.go @@ -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) } diff --git a/v2/optics/prism/prisms.go b/v2/optics/prism/prisms.go index 76aae22..795ea27 100644 --- a/v2/optics/prism/prisms.go +++ b/v2/optics/prism/prisms.go @@ -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. diff --git a/v2/optics/prism/types.go b/v2/optics/prism/types.go index b9f3e2f..f85afaf 100644 --- a/v2/optics/prism/types.go +++ b/v2/optics/prism/types.go @@ -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] ) diff --git a/v2/optics/traversal/doc.go b/v2/optics/traversal/doc.go index 331752f..cb9d50f 100644 --- a/v2/optics/traversal/doc.go +++ b/v2/optics/traversal/doc.go @@ -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)] diff --git a/v2/pair/doc.go b/v2/pair/doc.go index 5c52d68..fea2e97 100644 --- a/v2/pair/doc.go +++ b/v2/pair/doc.go @@ -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} diff --git a/v2/readerio/reader.go b/v2/readerio/reader.go index 2a481ec..fb24b66 100644 --- a/v2/readerio/reader.go +++ b/v2/readerio/reader.go @@ -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 // diff --git a/v2/readerio/reader_test.go b/v2/readerio/reader_test.go index 353ef19..4fc9129 100644 --- a/v2/readerio/reader_test.go +++ b/v2/readerio/reader_test.go @@ -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() {}) diff --git a/v2/readeroption/array.go b/v2/readeroption/array.go index 36c6226..172b29c 100644 --- a/v2/readeroption/array.go +++ b/v2/readeroption/array.go @@ -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) } diff --git a/v2/readeroption/bind.go b/v2/readeroption/bind.go index 3448faa..0147fa0 100644 --- a/v2/readeroption/bind.go +++ b/v2/readeroption/bind.go @@ -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) } diff --git a/v2/readeroption/reader.go b/v2/readeroption/reader.go index 2e580b1..a0c8008 100644 --- a/v2/readeroption/reader.go +++ b/v2/readeroption/reader.go @@ -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]) diff --git a/v2/readeroption/reader_test.go b/v2/readeroption/reader_test.go index 5abc058..d6f3c6e 100644 --- a/v2/readeroption/reader_test.go +++ b/v2/readeroption/reader_test.go @@ -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) } diff --git a/v2/readeroption/types.go b/v2/readeroption/types.go index 24ed1d1..f40b031 100644 --- a/v2/readeroption/types.go +++ b/v2/readeroption/types.go @@ -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] diff --git a/v2/samples/http/http_test.go b/v2/samples/http/http_test.go index 81f8696..e7e2e43 100644 --- a/v2/samples/http/http_test.go +++ b/v2/samples/http/http_test.go @@ -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]] { diff --git a/v2/samples/match/examples_match_test.go b/v2/samples/match/examples_match_test.go index 6858247..cb44ed7 100644 --- a/v2/samples/match/examples_match_test.go +++ b/v2/samples/match/examples_match_test.go @@ -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) diff --git a/v2/samples/mostly-adequate/chapter08_tupperware_test.go b/v2/samples/mostly-adequate/chapter08_tupperware_test.go index fbccf3b..c848a6f 100644 --- a/v2/samples/mostly-adequate/chapter08_tupperware_test.go +++ b/v2/samples/mostly-adequate/chapter08_tupperware_test.go @@ -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")), diff --git a/v2/samples/mostly-adequate/chapter09_monadiconions_test.go b/v2/samples/mostly-adequate/chapter09_monadiconions_test.go index 2538fed..7040989 100644 --- a/v2/samples/mostly-adequate/chapter09_monadiconions_test.go +++ b/v2/samples/mostly-adequate/chapter09_monadiconions_test.go @@ -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( diff --git a/v2/samples/mostly-adequate/types.go b/v2/samples/mostly-adequate/types.go index edcd6b1..5d79d00 100644 --- a/v2/samples/mostly-adequate/types.go +++ b/v2/samples/mostly-adequate/types.go @@ -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] ) diff --git a/v2/tuple/tuple_test.go b/v2/tuple/tuple_test.go index ef5a941..d2ed7fe 100644 --- a/v2/tuple/tuple_test.go +++ b/v2/tuple/tuple_test.go @@ -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)