mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-07 23:03:15 +02:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f652a94c3a | ||
|
|
774db88ca5 | ||
|
|
62a3365b20 | ||
|
|
d9a16a6771 | ||
|
|
8949cc7dca | ||
|
|
fa6b6caf22 | ||
|
|
a1e8d397c3 | ||
|
|
dbe7102e43 | ||
|
|
09aeb996e2 | ||
|
|
7cd575d95a | ||
|
|
dcfb023891 | ||
|
|
51cf241a26 | ||
|
|
9004c93976 | ||
|
|
d8ab6b0ce5 | ||
|
|
4e9998b645 |
@@ -314,7 +314,7 @@ if err != nil {
|
||||
|
||||
```go
|
||||
// Map transforms the success value
|
||||
double := result.Map(func(x int) int { return x * 2 })
|
||||
double := result.Map(N.Mul(2))
|
||||
result := double(result.Right[error](21)) // Right(42)
|
||||
|
||||
// Chain sequences operations
|
||||
@@ -330,7 +330,7 @@ validate := result.Chain(func(x int) result.Result[int] {
|
||||
|
||||
```go
|
||||
// Map transforms the success value
|
||||
double := result.Map(func(x int) int { return x * 2 })
|
||||
double := result.Map(N.Mul(2))
|
||||
value, err := double(21, nil) // (42, nil)
|
||||
|
||||
// Chain sequences operations
|
||||
|
||||
174
v2/IDIOMATIC_READERIORESULT_TODO.md
Normal file
174
v2/IDIOMATIC_READERIORESULT_TODO.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# Idiomatic ReadIOResult Functions - Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the idiomatic functions that should be added to the `readerioresult` package to support Go's native `(value, error)` pattern, similar to what was implemented for `readerresult`.
|
||||
|
||||
## Key Concepts
|
||||
|
||||
The idiomatic package `github.com/IBM/fp-go/v2/idiomatic/readerioresult` defines:
|
||||
- `ReaderIOResult[R, A]` as `func(R) func() (A, error)` (idiomatic style)
|
||||
- This contrasts with `readerioresult.ReaderIOResult[R, A]` which is `Reader[R, IOResult[A]]` (functional style)
|
||||
|
||||
## Functions to Add
|
||||
|
||||
### In `readerioresult/reader.go`
|
||||
|
||||
Add helper functions at the top:
|
||||
```go
|
||||
func fromReaderIOResultKleisliI[R, A, B any](f RIORI.Kleisli[R, A, B]) Kleisli[R, A, B] {
|
||||
return function.Flow2(f, FromReaderIOResultI[R, B])
|
||||
}
|
||||
|
||||
func fromIOResultKleisliI[A, B any](f IORI.Kleisli[A, B]) ioresult.Kleisli[A, B] {
|
||||
return ioresult.Eitherize1(f)
|
||||
}
|
||||
```
|
||||
|
||||
### Core Conversion Functions
|
||||
|
||||
1. **FromResultI** - Lift `(value, error)` to ReaderIOResult
|
||||
```go
|
||||
func FromResultI[R, A any](a A, err error) ReaderIOResult[R, A]
|
||||
```
|
||||
|
||||
2. **FromIOResultI** - Lift idiomatic IOResult to functional
|
||||
```go
|
||||
func FromIOResultI[R, A any](ioe func() (A, error)) ReaderIOResult[R, A]
|
||||
```
|
||||
|
||||
3. **FromReaderIOResultI** - Convert idiomatic ReaderIOResult to functional
|
||||
```go
|
||||
func FromReaderIOResultI[R, A any](rr RIORI.ReaderIOResult[R, A]) ReaderIOResult[R, A]
|
||||
```
|
||||
|
||||
### Chain Functions
|
||||
|
||||
4. **MonadChainI** / **ChainI** - Chain with idiomatic Kleisli
|
||||
```go
|
||||
func MonadChainI[R, A, B any](ma ReaderIOResult[R, A], f RIORI.Kleisli[R, A, B]) ReaderIOResult[R, B]
|
||||
func ChainI[R, A, B any](f RIORI.Kleisli[R, A, B]) Operator[R, A, B]
|
||||
```
|
||||
|
||||
5. **MonadChainEitherIK** / **ChainEitherIK** - Chain with idiomatic Result functions
|
||||
```go
|
||||
func MonadChainEitherIK[R, A, B any](ma ReaderIOResult[R, A], f func(A) (B, error)) ReaderIOResult[R, B]
|
||||
func ChainEitherIK[R, A, B any](f func(A) (B, error)) Operator[R, A, B]
|
||||
```
|
||||
|
||||
6. **MonadChainIOResultIK** / **ChainIOResultIK** - Chain with idiomatic IOResult
|
||||
```go
|
||||
func MonadChainIOResultIK[R, A, B any](ma ReaderIOResult[R, A], f func(A) func() (B, error)) ReaderIOResult[R, B]
|
||||
func ChainIOResultIK[R, A, B any](f func(A) func() (B, error)) Operator[R, A, B]
|
||||
```
|
||||
|
||||
### Applicative Functions
|
||||
|
||||
7. **MonadApI** / **ApI** - Apply with idiomatic value
|
||||
```go
|
||||
func MonadApI[B, R, A any](fab ReaderIOResult[R, func(A) B], fa RIORI.ReaderIOResult[R, A]) ReaderIOResult[R, B]
|
||||
func ApI[B, R, A any](fa RIORI.ReaderIOResult[R, A]) Operator[R, func(A) B, B]
|
||||
```
|
||||
|
||||
### Error Handling Functions
|
||||
|
||||
8. **OrElseI** - Fallback with idiomatic computation
|
||||
```go
|
||||
func OrElseI[R, A any](onLeft RIORI.Kleisli[R, error, A]) Operator[R, A, A]
|
||||
```
|
||||
|
||||
9. **MonadAltI** / **AltI** - Alternative with idiomatic computation
|
||||
```go
|
||||
func MonadAltI[R, A any](first ReaderIOResult[R, A], second Lazy[RIORI.ReaderIOResult[R, A]]) ReaderIOResult[R, A]
|
||||
func AltI[R, A any](second Lazy[RIORI.ReaderIOResult[R, A]]) Operator[R, A, A]
|
||||
```
|
||||
|
||||
### Flatten Functions
|
||||
|
||||
10. **FlattenI** - Flatten nested idiomatic ReaderIOResult
|
||||
```go
|
||||
func FlattenI[R, A any](mma ReaderIOResult[R, RIORI.ReaderIOResult[R, A]]) ReaderIOResult[R, A]
|
||||
```
|
||||
|
||||
### In `readerioresult/bind.go`
|
||||
|
||||
11. **BindI** - Bind with idiomatic Kleisli
|
||||
```go
|
||||
func BindI[R, S1, S2, T any](setter func(T) func(S1) S2, f RIORI.Kleisli[R, S1, T]) Operator[R, S1, S2]
|
||||
```
|
||||
|
||||
12. **ApIS** - Apply idiomatic value to state
|
||||
```go
|
||||
func ApIS[R, S1, S2, T any](setter func(T) func(S1) S2, fa RIORI.ReaderIOResult[R, T]) Operator[R, S1, S2]
|
||||
```
|
||||
|
||||
13. **ApISL** - Apply idiomatic value using lens
|
||||
```go
|
||||
func ApISL[R, S, T any](lens L.Lens[S, T], fa RIORI.ReaderIOResult[R, T]) Operator[R, S, S]
|
||||
```
|
||||
|
||||
14. **BindIL** - Bind idiomatic with lens
|
||||
```go
|
||||
func BindIL[R, S, T any](lens L.Lens[S, T], f RIORI.Kleisli[R, T, T]) Operator[R, S, S]
|
||||
```
|
||||
|
||||
15. **BindEitherIK** / **BindResultIK** - Bind idiomatic Result
|
||||
```go
|
||||
func BindEitherIK[R, S1, S2, T any](setter func(T) func(S1) S2, f func(S1) (T, error)) Operator[R, S1, S2]
|
||||
func BindResultIK[R, S1, S2, T any](setter func(T) func(S1) S2, f func(S1) (T, error)) Operator[R, S1, S2]
|
||||
```
|
||||
|
||||
16. **BindIOResultIK** - Bind idiomatic IOResult
|
||||
```go
|
||||
func BindIOResultIK[R, S1, S2, T any](setter func(T) func(S1) S2, f func(S1) func() (T, error)) Operator[R, S1, S2]
|
||||
```
|
||||
|
||||
17. **BindToEitherI** / **BindToResultI** - Initialize from idiomatic pair
|
||||
```go
|
||||
func BindToEitherI[R, S1, T any](setter func(T) S1) func(T, error) ReaderIOResult[R, S1]
|
||||
func BindToResultI[R, S1, T any](setter func(T) S1) func(T, error) ReaderIOResult[R, S1]
|
||||
```
|
||||
|
||||
18. **BindToIOResultI** - Initialize from idiomatic IOResult
|
||||
```go
|
||||
func BindToIOResultI[R, S1, T any](setter func(T) S1) func(func() (T, error)) ReaderIOResult[R, S1]
|
||||
```
|
||||
|
||||
19. **ApEitherIS** / **ApResultIS** - Apply idiomatic pair to state
|
||||
```go
|
||||
func ApEitherIS[R, S1, S2, T any](setter func(T) func(S1) S2) func(T, error) Operator[R, S1, S2]
|
||||
func ApResultIS[R, S1, S2, T any](setter func(T) func(S1) S2) func(T, error) Operator[R, S1, S2]
|
||||
```
|
||||
|
||||
20. **ApIOResultIS** - Apply idiomatic IOResult to state
|
||||
```go
|
||||
func ApIOResultIS[R, S1, S2, T any](setter func(T) func(S1) S2, fa func() (T, error)) Operator[R, S1, S2]
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Create `readerioresult/idiomatic_test.go` with:
|
||||
- Tests for each idiomatic function
|
||||
- Success and error cases
|
||||
- Integration tests showing real-world usage patterns
|
||||
- Parallel execution tests where applicable
|
||||
- Complex scenarios combining multiple idiomatic functions
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
1. **High Priority** - Core conversion and chain functions (1-6)
|
||||
2. **Medium Priority** - Bind functions for do-notation (11-16)
|
||||
3. **Low Priority** - Advanced applicative and error handling (7-10, 17-20)
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Seamless Integration** - Mix Go idiomatic code with functional pipelines
|
||||
2. **Gradual Adoption** - Convert code incrementally from idiomatic to functional
|
||||
3. **Interoperability** - Work with existing Go libraries that return `(value, error)`
|
||||
4. **Consistency** - Mirrors the successful pattern from `readerresult`
|
||||
|
||||
## References
|
||||
|
||||
- See `readerresult` package for similar implementations
|
||||
- See `idiomatic/readerresult` for the idiomatic types
|
||||
- See `idiomatic/ioresult` for IO-level idiomatic patterns
|
||||
@@ -205,7 +205,7 @@ The `Compose` function for endomorphisms now follows **mathematical function com
|
||||
```go
|
||||
// Compose executed left-to-right
|
||||
double := N.Mul(2)
|
||||
increment := func(x int) int { return x + 1 }
|
||||
increment := N.Add(1)
|
||||
composed := Compose(double, increment)
|
||||
result := composed(5) // (5 * 2) + 1 = 11
|
||||
```
|
||||
@@ -214,7 +214,7 @@ result := composed(5) // (5 * 2) + 1 = 11
|
||||
```go
|
||||
// Compose executes RIGHT-TO-LEFT (mathematical composition)
|
||||
double := N.Mul(2)
|
||||
increment := func(x int) int { return x + 1 }
|
||||
increment := N.Add(1)
|
||||
composed := Compose(double, increment)
|
||||
result := composed(5) // (5 + 1) * 2 = 12
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestReplicate(t *testing.T) {
|
||||
|
||||
func TestMonadMap(t *testing.T) {
|
||||
src := []int{1, 2, 3}
|
||||
result := MonadMap(src, func(x int) int { return x * 2 })
|
||||
result := MonadMap(src, N.Mul(2))
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
}
|
||||
|
||||
@@ -173,8 +173,8 @@ func TestChain(t *testing.T) {
|
||||
|
||||
func TestMonadAp(t *testing.T) {
|
||||
fns := []func(int) int{
|
||||
func(x int) int { return x * 2 },
|
||||
func(x int) int { return x + 10 },
|
||||
N.Mul(2),
|
||||
N.Add(10),
|
||||
}
|
||||
values := []int{1, 2}
|
||||
result := MonadAp(fns, values)
|
||||
@@ -268,7 +268,7 @@ func TestCopy(t *testing.T) {
|
||||
|
||||
func TestClone(t *testing.T) {
|
||||
src := []int{1, 2, 3}
|
||||
cloner := Clone(func(x int) int { return x * 2 })
|
||||
cloner := Clone(N.Mul(2))
|
||||
result := cloner(src)
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
}
|
||||
|
||||
470
v2/assert/assert.go
Normal file
470
v2/assert/assert.go
Normal file
@@ -0,0 +1,470 @@
|
||||
// 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 provides functional assertion helpers for testing.
|
||||
//
|
||||
// This package wraps testify/assert functions in a Reader monad pattern,
|
||||
// allowing for composable and functional test assertions. Each assertion
|
||||
// returns a Reader that takes a *testing.T and performs the assertion.
|
||||
//
|
||||
// The package supports:
|
||||
// - Equality and inequality assertions
|
||||
// - Collection assertions (arrays, maps, strings)
|
||||
// - Error handling assertions
|
||||
// - Result type assertions
|
||||
// - Custom predicate assertions
|
||||
// - Composable test suites
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestExample(t *testing.T) {
|
||||
// value := 42
|
||||
// assert.Equal(42)(value)(t) // Curried style
|
||||
//
|
||||
// // Composing multiple assertions
|
||||
// arr := []int{1, 2, 3}
|
||||
// assertions := assert.AllOf([]assert.Reader{
|
||||
// assert.ArrayNotEmpty(arr),
|
||||
// assert.ArrayLength[int](3)(arr),
|
||||
// assert.ArrayContains(2)(arr),
|
||||
// })
|
||||
// assertions(t)
|
||||
// }
|
||||
package assert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/boolean"
|
||||
"github.com/IBM/fp-go/v2/eq"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
// Eq is the equal predicate checking if objects are equal
|
||||
Eq = eq.FromEquals(assert.ObjectsAreEqual)
|
||||
)
|
||||
|
||||
// wrap1 is an internal helper function that wraps testify assertion functions
|
||||
// into the Reader monad pattern with curried parameters.
|
||||
//
|
||||
// It takes a testify assertion function and converts it into a curried function
|
||||
// that first takes an expected value, then an actual value, and finally returns
|
||||
// a Reader that performs the assertion when given a *testing.T.
|
||||
//
|
||||
// Parameters:
|
||||
// - wrapped: The testify assertion function to wrap
|
||||
// - expected: The expected value for comparison
|
||||
// - msgAndArgs: Optional message and arguments for assertion failure
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli function that takes the actual value and returns a Reader
|
||||
func wrap1[T any](wrapped func(t assert.TestingT, expected, actual any, msgAndArgs ...any) bool, expected T, msgAndArgs ...any) Kleisli[T] {
|
||||
return func(actual T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return wrapped(t, expected, actual, msgAndArgs...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NotEqual tests if the expected and the actual values are not equal
|
||||
func NotEqual[T any](expected T) Kleisli[T] {
|
||||
return wrap1(assert.NotEqual, expected)
|
||||
}
|
||||
|
||||
// Equal tests if the expected and the actual values are equal
|
||||
func Equal[T any](expected T) Kleisli[T] {
|
||||
return wrap1(assert.Equal, expected)
|
||||
}
|
||||
|
||||
// ArrayNotEmpty checks if an array is not empty
|
||||
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
|
||||
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
|
||||
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
|
||||
func ArrayLength[T any](expected int) Kleisli[[]T] {
|
||||
return func(actual []T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.Len(t, actual, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RecordLength tests if a map has the expected length
|
||||
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 {
|
||||
return assert.Len(t, actual, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// StringLength tests if a string has the expected length
|
||||
func StringLength[K comparable, T any](expected int) Kleisli[string] {
|
||||
return func(actual string) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.Len(t, actual, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NoError validates that there is no error
|
||||
func NoError(err error) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Error validates that there is an error
|
||||
func Error(err error) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Success checks if a [Result] represents success
|
||||
func Success[T any](res Result[T]) Reader {
|
||||
return NoError(result.ToError(res))
|
||||
}
|
||||
|
||||
// Failure checks if a [Result] represents failure
|
||||
func Failure[T any](res Result[T]) Reader {
|
||||
return Error(result.ToError(res))
|
||||
}
|
||||
|
||||
// ArrayContains tests if a value is contained in an array
|
||||
func ArrayContains[T any](expected T) Kleisli[[]T] {
|
||||
return func(actual []T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.Contains(t, actual, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ContainsKey tests if a key is contained in a map
|
||||
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 {
|
||||
return assert.Contains(t, actual, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NotContainsKey tests if a key is not contained in a map
|
||||
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 {
|
||||
return assert.NotContains(t, actual, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// That asserts that a particular predicate matches
|
||||
func That[T any](pred Predicate[T]) Kleisli[T] {
|
||||
return func(a T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
if pred(a) {
|
||||
return true
|
||||
}
|
||||
return assert.Fail(t, fmt.Sprintf("Preficate %v does not match value %v", pred, a))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AllOf combines multiple assertion Readers into a single Reader that passes
|
||||
// only if all assertions pass.
|
||||
//
|
||||
// This function uses boolean AND logic (MonoidAll) to combine the results of
|
||||
// all assertions. If any assertion fails, the combined assertion fails.
|
||||
//
|
||||
// This is useful for grouping related assertions together and ensuring all
|
||||
// conditions are met.
|
||||
//
|
||||
// Parameters:
|
||||
// - readers: Array of assertion Readers to combine
|
||||
//
|
||||
// Returns:
|
||||
// - A single Reader that performs all assertions and returns true only if all pass
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestUser(t *testing.T) {
|
||||
// user := User{Name: "Alice", Age: 30, Active: true}
|
||||
// assertions := assert.AllOf([]assert.Reader{
|
||||
// assert.Equal("Alice")(user.Name),
|
||||
// assert.Equal(30)(user.Age),
|
||||
// assert.Equal(true)(user.Active),
|
||||
// })
|
||||
// assertions(t)
|
||||
// }
|
||||
//
|
||||
//go:inline
|
||||
func AllOf(readers []Reader) Reader {
|
||||
return reader.MonadReduceArrayM(readers, boolean.MonoidAll)
|
||||
}
|
||||
|
||||
// RunAll executes a map of named test cases, running each as a subtest.
|
||||
//
|
||||
// This function creates a Reader that runs multiple named test cases using
|
||||
// Go's t.Run for proper test isolation and reporting. Each test case is
|
||||
// executed as a separate subtest with its own name.
|
||||
//
|
||||
// The function returns true only if all subtests pass. This allows for
|
||||
// better test organization and clearer test output.
|
||||
//
|
||||
// Parameters:
|
||||
// - testcases: Map of test names to assertion Readers
|
||||
//
|
||||
// Returns:
|
||||
// - A Reader that executes all named test cases and returns true if all pass
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestMathOperations(t *testing.T) {
|
||||
// testcases := map[string]assert.Reader{
|
||||
// "addition": assert.Equal(4)(2 + 2),
|
||||
// "multiplication": assert.Equal(6)(2 * 3),
|
||||
// "subtraction": assert.Equal(1)(3 - 2),
|
||||
// }
|
||||
// assert.RunAll(testcases)(t)
|
||||
// }
|
||||
//
|
||||
//go:inline
|
||||
func RunAll(testcases map[string]Reader) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
current := true
|
||||
for k, r := range testcases {
|
||||
current = current && t.Run(k, func(t1 *testing.T) {
|
||||
r(t1)
|
||||
})
|
||||
}
|
||||
return current
|
||||
}
|
||||
}
|
||||
|
||||
// Local transforms a Reader that works on type R1 into a Reader that works on type R2,
|
||||
// by providing a function that converts R2 to R1. This allows you to focus a test on a
|
||||
// specific property or subset of a larger data structure.
|
||||
//
|
||||
// This is particularly useful when you have an assertion that operates on a specific field
|
||||
// or property, and you want to apply it to a complete object. Instead of extracting the
|
||||
// property and then asserting on it, you can transform the assertion to work directly
|
||||
// on the whole object.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that extracts or transforms R2 into R1
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms a Reader[R1, Reader] into a Reader[R2, Reader]
|
||||
//
|
||||
// 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)
|
||||
//
|
||||
//go:inline
|
||||
func Local[R1, R2 any](f func(R2) R1) func(Kleisli[R1]) Kleisli[R2] {
|
||||
return reader.Local[Reader](f)
|
||||
}
|
||||
|
||||
// LocalL is similar to Local but uses a Lens to focus on a specific property.
|
||||
// A Lens is a functional programming construct that provides a composable way to
|
||||
// focus on a part of a data structure.
|
||||
//
|
||||
// This function is particularly useful when you want to focus a test on a specific
|
||||
// field of a struct using a lens, making the code more declarative and composable.
|
||||
// Lenses are often code-generated or predefined for common data structures.
|
||||
//
|
||||
// Parameters:
|
||||
// - l: A Lens that focuses from type S to type T
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms a Reader[T, Reader] into a Reader[S, Reader]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Email string
|
||||
// }
|
||||
//
|
||||
// // Assume we have a lens that focuses on the Email field
|
||||
// var emailLens = lens.Prop[Person, string]("Email")
|
||||
//
|
||||
// // Create an assertion for email format
|
||||
// validEmail := assert.That(func(email string) bool {
|
||||
// return strings.Contains(email, "@")
|
||||
// })
|
||||
//
|
||||
// // Focus this assertion on the Email property using a lens
|
||||
// validPersonEmail := assert.LocalL(emailLens)(validEmail)
|
||||
//
|
||||
// // Test a Person object
|
||||
// person := Person{Name: "Bob", Email: "bob@example.com"}
|
||||
// validPersonEmail(person)(t)
|
||||
//
|
||||
//go:inline
|
||||
func LocalL[S, T any](l Lens[S, T]) func(Kleisli[T]) Kleisli[S] {
|
||||
return reader.Local[Reader](l.Get)
|
||||
}
|
||||
|
||||
// fromOptionalGetter is an internal helper that creates an assertion Reader from
|
||||
// an optional getter function. It asserts that the optional value is present (Some).
|
||||
func fromOptionalGetter[S, T any](getter func(S) option.Option[T], msgAndArgs ...any) Kleisli[S] {
|
||||
return func(s S) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.True(t, option.IsSome(getter(s)), msgAndArgs...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FromOptional creates an assertion that checks if an Optional can successfully extract a value.
|
||||
// An Optional is an optic that represents an optional reference to a subpart of a data structure.
|
||||
//
|
||||
// This function is useful when you have an Optional optic and want to assert that the optional
|
||||
// value is present (Some) rather than absent (None). The assertion passes if the Optional's
|
||||
// GetOption returns Some, and fails if it returns None.
|
||||
//
|
||||
// This enables property-focused testing where you verify that a particular optional field or
|
||||
// sub-structure exists and is accessible.
|
||||
//
|
||||
// Parameters:
|
||||
// - opt: An Optional optic that focuses from type S to type T
|
||||
//
|
||||
// Returns:
|
||||
// - A Reader that asserts the optional value is present when applied to a value of type S
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Database *DatabaseConfig // Optional field
|
||||
// }
|
||||
//
|
||||
// type DatabaseConfig struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
//
|
||||
// // Create an Optional that focuses on the Database field
|
||||
// dbOptional := optional.MakeOptional(
|
||||
// func(c Config) option.Option[*DatabaseConfig] {
|
||||
// if c.Database != nil {
|
||||
// return option.Some(c.Database)
|
||||
// }
|
||||
// return option.None[*DatabaseConfig]()
|
||||
// },
|
||||
// func(c Config, db *DatabaseConfig) Config {
|
||||
// c.Database = db
|
||||
// return c
|
||||
// },
|
||||
// )
|
||||
//
|
||||
// // Assert that the database config is present
|
||||
// hasDatabaseConfig := assert.FromOptional(dbOptional)
|
||||
//
|
||||
// config := Config{Database: &DatabaseConfig{Host: "localhost", Port: 5432}}
|
||||
// hasDatabaseConfig(config)(t) // Passes
|
||||
//
|
||||
// emptyConfig := Config{Database: nil}
|
||||
// hasDatabaseConfig(emptyConfig)(t) // Fails
|
||||
//
|
||||
//go:inline
|
||||
func FromOptional[S, T any](opt Optional[S, T]) reader.Reader[S, Reader] {
|
||||
return fromOptionalGetter(opt.GetOption, "Optional: %s", opt)
|
||||
}
|
||||
|
||||
// FromPrism creates an assertion that checks if a Prism can successfully extract a value.
|
||||
// A Prism is an optic used to select part of a sum type (tagged union or variant).
|
||||
//
|
||||
// This function is useful when you have a Prism optic and want to assert that a value
|
||||
// matches a specific variant of a sum type. The assertion passes if the Prism's GetOption
|
||||
// returns Some (meaning the value is of the expected variant), and fails if it returns None
|
||||
// (meaning the value is a different variant).
|
||||
//
|
||||
// This enables variant-focused testing where you verify that a value is of a particular
|
||||
// type or matches a specific condition within a sum type.
|
||||
//
|
||||
// Parameters:
|
||||
// - p: A Prism optic that focuses from type S to type T
|
||||
//
|
||||
// Returns:
|
||||
// - A Reader that asserts the prism successfully extracts when applied to a value of type S
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Result interface{ isResult() }
|
||||
// type Success struct{ Value int }
|
||||
// type Failure struct{ Error string }
|
||||
//
|
||||
// func (Success) isResult() {}
|
||||
// func (Failure) isResult() {}
|
||||
//
|
||||
// // Create a Prism that focuses on Success variant
|
||||
// successPrism := prism.MakePrism(
|
||||
// func(r Result) option.Option[int] {
|
||||
// if s, ok := r.(Success); ok {
|
||||
// return option.Some(s.Value)
|
||||
// }
|
||||
// return option.None[int]()
|
||||
// },
|
||||
// func(v int) Result { return Success{Value: v} },
|
||||
// )
|
||||
//
|
||||
// // Assert that the result is a Success
|
||||
// isSuccess := assert.FromPrism(successPrism)
|
||||
//
|
||||
// result1 := Success{Value: 42}
|
||||
// isSuccess(result1)(t) // Passes
|
||||
//
|
||||
// result2 := Failure{Error: "something went wrong"}
|
||||
// isSuccess(result2)(t) // Fails
|
||||
//
|
||||
//go:inline
|
||||
func FromPrism[S, T any](p Prism[S, T]) reader.Reader[S, Reader] {
|
||||
return fromOptionalGetter(p.GetOption, "Prism: %s", p)
|
||||
}
|
||||
@@ -16,94 +16,676 @@
|
||||
package assert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/eq"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
errTest = fmt.Errorf("test failure")
|
||||
|
||||
// Eq is the equal predicate checking if objects are equal
|
||||
Eq = eq.FromEquals(assert.ObjectsAreEqual)
|
||||
)
|
||||
|
||||
func wrap1[T any](wrapped func(t assert.TestingT, expected, actual any, msgAndArgs ...any) bool, t *testing.T, expected T) result.Kleisli[T, T] {
|
||||
return func(actual T) Result[T] {
|
||||
ok := wrapped(t, expected, actual)
|
||||
if ok {
|
||||
return result.Of(actual)
|
||||
func TestEqual(t *testing.T) {
|
||||
t.Run("should pass when values are equal", func(t *testing.T) {
|
||||
result := Equal(42)(42)(t)
|
||||
if !result {
|
||||
t.Error("Expected Equal to pass for equal values")
|
||||
}
|
||||
return result.Left[T](errTest)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// NotEqual tests if the expected and the actual values are not equal
|
||||
func NotEqual[T any](t *testing.T, expected T) result.Kleisli[T, T] {
|
||||
return wrap1(assert.NotEqual, t, expected)
|
||||
}
|
||||
|
||||
// Equal tests if the expected and the actual values are equal
|
||||
func Equal[T any](t *testing.T, expected T) result.Kleisli[T, T] {
|
||||
return wrap1(assert.Equal, t, expected)
|
||||
}
|
||||
|
||||
// Length tests if an array has the expected length
|
||||
func Length[T any](t *testing.T, expected int) result.Kleisli[[]T, []T] {
|
||||
return func(actual []T) Result[[]T] {
|
||||
ok := assert.Len(t, actual, expected)
|
||||
if ok {
|
||||
return result.Of(actual)
|
||||
t.Run("should fail when values are not equal", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
result := Equal(42)(43)(mockT)
|
||||
if result {
|
||||
t.Error("Expected Equal to fail for different values")
|
||||
}
|
||||
return result.Left[[]T](errTest)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with strings", func(t *testing.T) {
|
||||
result := Equal("hello")("hello")(t)
|
||||
if !result {
|
||||
t.Error("Expected Equal to pass for equal strings")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// NoError validates that there is no error
|
||||
func NoError[T any](t *testing.T) result.Operator[T, T] {
|
||||
return func(actual Result[T]) Result[T] {
|
||||
return result.MonadFold(actual, func(e error) Result[T] {
|
||||
assert.NoError(t, e)
|
||||
return result.Left[T](e)
|
||||
}, func(value T) Result[T] {
|
||||
assert.NoError(t, nil)
|
||||
return result.Of(value)
|
||||
func TestNotEqual(t *testing.T) {
|
||||
t.Run("should pass when values are not equal", func(t *testing.T) {
|
||||
result := NotEqual(42)(43)(t)
|
||||
if !result {
|
||||
t.Error("Expected NotEqual to pass for different values")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when values are equal", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
result := NotEqual(42)(42)(mockT)
|
||||
if result {
|
||||
t.Error("Expected NotEqual to fail for equal values")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayNotEmpty(t *testing.T) {
|
||||
t.Run("should pass for non-empty array", func(t *testing.T) {
|
||||
arr := []int{1, 2, 3}
|
||||
result := ArrayNotEmpty(arr)(t)
|
||||
if !result {
|
||||
t.Error("Expected ArrayNotEmpty to pass for non-empty array")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail for empty array", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
arr := []int{}
|
||||
result := ArrayNotEmpty(arr)(mockT)
|
||||
if result {
|
||||
t.Error("Expected ArrayNotEmpty to fail for empty array")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRecordNotEmpty(t *testing.T) {
|
||||
t.Run("should pass for non-empty map", func(t *testing.T) {
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
result := RecordNotEmpty(mp)(t)
|
||||
if !result {
|
||||
t.Error("Expected RecordNotEmpty to pass for non-empty map")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail for empty map", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
mp := map[string]int{}
|
||||
result := RecordNotEmpty(mp)(mockT)
|
||||
if result {
|
||||
t.Error("Expected RecordNotEmpty to fail for empty map")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayLength(t *testing.T) {
|
||||
t.Run("should pass when length matches", func(t *testing.T) {
|
||||
arr := []int{1, 2, 3}
|
||||
result := ArrayLength[int](3)(arr)(t)
|
||||
if !result {
|
||||
t.Error("Expected ArrayLength to pass when length matches")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when length doesn't match", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
arr := []int{1, 2, 3}
|
||||
result := ArrayLength[int](5)(arr)(mockT)
|
||||
if result {
|
||||
t.Error("Expected ArrayLength to fail when length doesn't match")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with empty array", func(t *testing.T) {
|
||||
arr := []string{}
|
||||
result := ArrayLength[string](0)(arr)(t)
|
||||
if !result {
|
||||
t.Error("Expected ArrayLength to pass for empty array with expected length 0")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRecordLength(t *testing.T) {
|
||||
t.Run("should pass when map length matches", func(t *testing.T) {
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
result := RecordLength[string, int](2)(mp)(t)
|
||||
if !result {
|
||||
t.Error("Expected RecordLength to pass when length matches")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when map length doesn't match", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
mp := map[string]int{"a": 1}
|
||||
result := RecordLength[string, int](3)(mp)(mockT)
|
||||
if result {
|
||||
t.Error("Expected RecordLength to fail when length doesn't match")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestStringLength(t *testing.T) {
|
||||
t.Run("should pass when string length matches", func(t *testing.T) {
|
||||
str := "hello"
|
||||
result := StringLength[string, int](5)(str)(t)
|
||||
if !result {
|
||||
t.Error("Expected StringLength to pass when length matches")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when string length doesn't match", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
str := "hello"
|
||||
result := StringLength[string, int](10)(str)(mockT)
|
||||
if result {
|
||||
t.Error("Expected StringLength to fail when length doesn't match")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with empty string", func(t *testing.T) {
|
||||
str := ""
|
||||
result := StringLength[string, int](0)(str)(t)
|
||||
if !result {
|
||||
t.Error("Expected StringLength to pass for empty string with expected length 0")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNoError(t *testing.T) {
|
||||
t.Run("should pass when error is nil", func(t *testing.T) {
|
||||
result := NoError(nil)(t)
|
||||
if !result {
|
||||
t.Error("Expected NoError to pass when error is nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when error is not nil", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
err := errors.New("test error")
|
||||
result := NoError(err)(mockT)
|
||||
if result {
|
||||
t.Error("Expected NoError to fail when error is not nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
t.Run("should pass when error is not nil", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
result := Error(err)(t)
|
||||
if !result {
|
||||
t.Error("Expected Error to pass when error is not nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when error is nil", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
result := Error(nil)(mockT)
|
||||
if result {
|
||||
t.Error("Expected Error to fail when error is nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSuccess(t *testing.T) {
|
||||
t.Run("should pass for successful result", func(t *testing.T) {
|
||||
res := result.Of(42)
|
||||
result := Success(res)(t)
|
||||
if !result {
|
||||
t.Error("Expected Success to pass for successful result")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail for error result", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
res := result.Left[int](errors.New("test error"))
|
||||
result := Success(res)(mockT)
|
||||
if result {
|
||||
t.Error("Expected Success to fail for error result")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFailure(t *testing.T) {
|
||||
t.Run("should pass for error result", func(t *testing.T) {
|
||||
res := result.Left[int](errors.New("test error"))
|
||||
result := Failure(res)(t)
|
||||
if !result {
|
||||
t.Error("Expected Failure to pass for error result")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail for successful result", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
res := result.Of(42)
|
||||
result := Failure(res)(mockT)
|
||||
if result {
|
||||
t.Error("Expected Failure to fail for successful result")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayContains(t *testing.T) {
|
||||
t.Run("should pass when element is in array", func(t *testing.T) {
|
||||
arr := []int{1, 2, 3, 4, 5}
|
||||
result := ArrayContains(3)(arr)(t)
|
||||
if !result {
|
||||
t.Error("Expected ArrayContains to pass when element is in array")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when element is not in array", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
arr := []int{1, 2, 3}
|
||||
result := ArrayContains(10)(arr)(mockT)
|
||||
if result {
|
||||
t.Error("Expected ArrayContains to fail when element is not in array")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with strings", func(t *testing.T) {
|
||||
arr := []string{"apple", "banana", "cherry"}
|
||||
result := ArrayContains("banana")(arr)(t)
|
||||
if !result {
|
||||
t.Error("Expected ArrayContains to pass for string element")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestContainsKey(t *testing.T) {
|
||||
t.Run("should pass when key exists in map", func(t *testing.T) {
|
||||
mp := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
result := ContainsKey[int]("b")(mp)(t)
|
||||
if !result {
|
||||
t.Error("Expected ContainsKey to pass when key exists")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when key doesn't exist in map", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
result := ContainsKey[int]("z")(mp)(mockT)
|
||||
if result {
|
||||
t.Error("Expected ContainsKey to fail when key doesn't exist")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNotContainsKey(t *testing.T) {
|
||||
t.Run("should pass when key doesn't exist in map", func(t *testing.T) {
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
result := NotContainsKey[int]("z")(mp)(t)
|
||||
if !result {
|
||||
t.Error("Expected NotContainsKey to pass when key doesn't exist")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when key exists in map", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
result := NotContainsKey[int]("a")(mp)(mockT)
|
||||
if result {
|
||||
t.Error("Expected NotContainsKey to fail when key exists")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestThat(t *testing.T) {
|
||||
t.Run("should pass when predicate is true", func(t *testing.T) {
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
result := That(isEven)(42)(t)
|
||||
if !result {
|
||||
t.Error("Expected That to pass when predicate is true")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when predicate is false", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
result := That(isEven)(43)(mockT)
|
||||
if result {
|
||||
t.Error("Expected That to fail when predicate is false")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with string predicates", func(t *testing.T) {
|
||||
startsWithH := func(s string) bool { return len(s) > 0 && s[0] == 'h' }
|
||||
result := That(startsWithH)("hello")(t)
|
||||
if !result {
|
||||
t.Error("Expected That to pass for string predicate")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAllOf(t *testing.T) {
|
||||
t.Run("should pass when all assertions pass", func(t *testing.T) {
|
||||
assertions := AllOf([]Reader{
|
||||
Equal(42)(42),
|
||||
Equal("hello")("hello"),
|
||||
ArrayNotEmpty([]int{1, 2, 3}),
|
||||
})
|
||||
}
|
||||
result := assertions(t)
|
||||
if !result {
|
||||
t.Error("Expected AllOf to pass when all assertions pass")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when any assertion fails", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
assertions := AllOf([]Reader{
|
||||
Equal(42)(42),
|
||||
Equal("hello")("goodbye"),
|
||||
ArrayNotEmpty([]int{1, 2, 3}),
|
||||
})
|
||||
result := assertions(mockT)
|
||||
if result {
|
||||
t.Error("Expected AllOf to fail when any assertion fails")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with empty array", func(t *testing.T) {
|
||||
assertions := AllOf([]Reader{})
|
||||
result := assertions(t)
|
||||
if !result {
|
||||
t.Error("Expected AllOf to pass for empty array")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should combine multiple array assertions", func(t *testing.T) {
|
||||
arr := []int{1, 2, 3, 4, 5}
|
||||
assertions := AllOf([]Reader{
|
||||
ArrayNotEmpty(arr),
|
||||
ArrayLength[int](5)(arr),
|
||||
ArrayContains(3)(arr),
|
||||
})
|
||||
result := assertions(t)
|
||||
if !result {
|
||||
t.Error("Expected AllOf to pass for multiple array assertions")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ArrayContains tests if a value is contained in an array
|
||||
func ArrayContains[T any](t *testing.T, expected T) result.Kleisli[[]T, []T] {
|
||||
return func(actual []T) Result[[]T] {
|
||||
ok := assert.Contains(t, actual, expected)
|
||||
if ok {
|
||||
return result.Of(actual)
|
||||
func TestRunAll(t *testing.T) {
|
||||
t.Run("should run all named test cases", func(t *testing.T) {
|
||||
testcases := map[string]Reader{
|
||||
"equality": Equal(42)(42),
|
||||
"string_check": Equal("test")("test"),
|
||||
"array_check": ArrayNotEmpty([]int{1, 2, 3}),
|
||||
}
|
||||
return result.Left[[]T](errTest)
|
||||
}
|
||||
result := RunAll(testcases)(t)
|
||||
if !result {
|
||||
t.Error("Expected RunAll to pass when all test cases pass")
|
||||
}
|
||||
})
|
||||
|
||||
// Note: Testing failure behavior of RunAll is tricky because subtests
|
||||
// will actually fail in the test framework. The function works correctly
|
||||
// as demonstrated by the passing test above.
|
||||
|
||||
t.Run("should work with empty test cases", func(t *testing.T) {
|
||||
testcases := map[string]Reader{}
|
||||
result := RunAll(testcases)(t)
|
||||
if !result {
|
||||
t.Error("Expected RunAll to pass for empty test cases")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ContainsKey tests if a key is contained in a map
|
||||
func ContainsKey[T any, K comparable](t *testing.T, expected K) result.Kleisli[map[K]T, map[K]T] {
|
||||
return func(actual map[K]T) Result[map[K]T] {
|
||||
ok := assert.Contains(t, actual, expected)
|
||||
if ok {
|
||||
return result.Of(actual)
|
||||
func TestEq(t *testing.T) {
|
||||
t.Run("should return true for equal values", func(t *testing.T) {
|
||||
if !Eq.Equals(42, 42) {
|
||||
t.Error("Expected Eq to return true for equal integers")
|
||||
}
|
||||
return result.Left[map[K]T](errTest)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should return false for different values", func(t *testing.T) {
|
||||
if Eq.Equals(42, 43) {
|
||||
t.Error("Expected Eq to return false for different integers")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with strings", func(t *testing.T) {
|
||||
if !Eq.Equals("hello", "hello") {
|
||||
t.Error("Expected Eq to return true for equal strings")
|
||||
}
|
||||
if Eq.Equals("hello", "world") {
|
||||
t.Error("Expected Eq to return false for different strings")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with slices", func(t *testing.T) {
|
||||
arr1 := []int{1, 2, 3}
|
||||
arr2 := []int{1, 2, 3}
|
||||
if !Eq.Equals(arr1, arr2) {
|
||||
t.Error("Expected Eq to return true for equal slices")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// NotContainsKey tests if a key is not contained in a map
|
||||
func NotContainsKey[T any, K comparable](t *testing.T, expected K) result.Kleisli[map[K]T, map[K]T] {
|
||||
return func(actual map[K]T) Result[map[K]T] {
|
||||
ok := assert.NotContains(t, actual, expected)
|
||||
if ok {
|
||||
return result.Of(actual)
|
||||
}
|
||||
return result.Left[map[K]T](errTest)
|
||||
func TestLocal(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("should focus assertion on a property", func(t *testing.T) {
|
||||
// Create an assertion that checks if age is positive
|
||||
ageIsPositive := That(func(age int) bool { return age > 0 })
|
||||
|
||||
// Focus this assertion on the Age field of User
|
||||
userAgeIsPositive := Local(func(u User) int { return u.Age })(ageIsPositive)
|
||||
|
||||
// Test with a user who has a positive age
|
||||
user := User{Name: "Alice", Age: 30}
|
||||
result := userAgeIsPositive(user)(t)
|
||||
if !result {
|
||||
t.Error("Expected focused assertion to pass for positive age")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when focused property doesn't match", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
ageIsPositive := That(func(age int) bool { return age > 0 })
|
||||
userAgeIsPositive := Local(func(u User) int { return u.Age })(ageIsPositive)
|
||||
|
||||
// Test with a user who has zero age
|
||||
user := User{Name: "Bob", Age: 0}
|
||||
result := userAgeIsPositive(user)(mockT)
|
||||
if result {
|
||||
t.Error("Expected focused assertion to fail for zero age")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should compose with other assertions", func(t *testing.T) {
|
||||
// Create multiple focused assertions
|
||||
nameNotEmpty := Local(func(u User) string { return u.Name })(
|
||||
That(func(name string) bool { return len(name) > 0 }),
|
||||
)
|
||||
ageInRange := Local(func(u User) int { return u.Age })(
|
||||
That(func(age int) bool { return age >= 18 && age <= 100 }),
|
||||
)
|
||||
|
||||
user := User{Name: "Charlie", Age: 25}
|
||||
assertions := AllOf([]Reader{
|
||||
nameNotEmpty(user),
|
||||
ageInRange(user),
|
||||
})
|
||||
|
||||
result := assertions(t)
|
||||
if !result {
|
||||
t.Error("Expected composed focused assertions to pass")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with Equal assertion", func(t *testing.T) {
|
||||
// Focus Equal assertion on Name field
|
||||
nameIsAlice := Local(func(u User) string { return u.Name })(Equal("Alice"))
|
||||
|
||||
user := User{Name: "Alice", Age: 30}
|
||||
result := nameIsAlice(user)(t)
|
||||
if !result {
|
||||
t.Error("Expected focused Equal assertion to pass")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLocalL(t *testing.T) {
|
||||
// Note: LocalL requires lens package which provides lens operations.
|
||||
// This test demonstrates the concept, but actual usage would require
|
||||
// proper lens definitions.
|
||||
|
||||
t.Run("conceptual test for LocalL", func(t *testing.T) {
|
||||
// LocalL is similar to Local but uses lenses for focusing.
|
||||
// It would be used like:
|
||||
// validEmail := That(func(email string) bool { return strings.Contains(email, "@") })
|
||||
// validPersonEmail := LocalL(emailLens)(validEmail)
|
||||
//
|
||||
// The actual implementation would require lens definitions from the lens package.
|
||||
// This test serves as documentation for the intended usage.
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromOptional(t *testing.T) {
|
||||
type DatabaseConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Database *DatabaseConfig
|
||||
}
|
||||
|
||||
// Create an Optional that focuses on the Database field
|
||||
dbOptional := Optional[Config, *DatabaseConfig]{
|
||||
GetOption: func(c Config) option.Option[*DatabaseConfig] {
|
||||
if c.Database != nil {
|
||||
return option.Of(c.Database)
|
||||
}
|
||||
return option.None[*DatabaseConfig]()
|
||||
},
|
||||
Set: func(db *DatabaseConfig) func(Config) Config {
|
||||
return func(c Config) Config {
|
||||
c.Database = db
|
||||
return c
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("should pass when optional value is present", func(t *testing.T) {
|
||||
config := Config{Database: &DatabaseConfig{Host: "localhost", Port: 5432}}
|
||||
hasDatabaseConfig := FromOptional(dbOptional)
|
||||
result := hasDatabaseConfig(config)(t)
|
||||
if !result {
|
||||
t.Error("Expected FromOptional to pass when optional value is present")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when optional value is absent", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
emptyConfig := Config{Database: nil}
|
||||
hasDatabaseConfig := FromOptional(dbOptional)
|
||||
result := hasDatabaseConfig(emptyConfig)(mockT)
|
||||
if result {
|
||||
t.Error("Expected FromOptional to fail when optional value is absent")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with nested optionals", func(t *testing.T) {
|
||||
type AdvancedSettings struct {
|
||||
Cache bool
|
||||
}
|
||||
|
||||
type Settings struct {
|
||||
Advanced *AdvancedSettings
|
||||
}
|
||||
|
||||
advancedOptional := Optional[Settings, *AdvancedSettings]{
|
||||
GetOption: func(s Settings) option.Option[*AdvancedSettings] {
|
||||
if s.Advanced != nil {
|
||||
return option.Of(s.Advanced)
|
||||
}
|
||||
return option.None[*AdvancedSettings]()
|
||||
},
|
||||
Set: func(adv *AdvancedSettings) func(Settings) Settings {
|
||||
return func(s Settings) Settings {
|
||||
s.Advanced = adv
|
||||
return s
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
settings := Settings{Advanced: &AdvancedSettings{Cache: true}}
|
||||
hasAdvanced := FromOptional(advancedOptional)
|
||||
result := hasAdvanced(settings)(t)
|
||||
if !result {
|
||||
t.Error("Expected FromOptional to pass for nested optional")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Helper types for Prism testing
|
||||
type PrismTestResult interface {
|
||||
isPrismTestResult()
|
||||
}
|
||||
|
||||
type PrismTestSuccess struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
type PrismTestFailure struct {
|
||||
Error string
|
||||
}
|
||||
|
||||
func (PrismTestSuccess) isPrismTestResult() {}
|
||||
func (PrismTestFailure) isPrismTestResult() {}
|
||||
|
||||
func TestFromPrism(t *testing.T) {
|
||||
// Create a Prism that focuses on Success variant using prism.MakePrism
|
||||
successPrism := prism.MakePrism(
|
||||
func(r PrismTestResult) option.Option[int] {
|
||||
if s, ok := r.(PrismTestSuccess); ok {
|
||||
return option.Of(s.Value)
|
||||
}
|
||||
return option.None[int]()
|
||||
},
|
||||
func(v int) PrismTestResult {
|
||||
return PrismTestSuccess{Value: v}
|
||||
},
|
||||
)
|
||||
|
||||
// Create a Prism that focuses on Failure variant
|
||||
failurePrism := prism.MakePrism(
|
||||
func(r PrismTestResult) option.Option[string] {
|
||||
if f, ok := r.(PrismTestFailure); ok {
|
||||
return option.Of(f.Error)
|
||||
}
|
||||
return option.None[string]()
|
||||
},
|
||||
func(err string) PrismTestResult {
|
||||
return PrismTestFailure{Error: err}
|
||||
},
|
||||
)
|
||||
|
||||
t.Run("should pass when prism successfully extracts", func(t *testing.T) {
|
||||
result := PrismTestSuccess{Value: 42}
|
||||
isSuccess := FromPrism(successPrism)
|
||||
testResult := isSuccess(result)(t)
|
||||
if !testResult {
|
||||
t.Error("Expected FromPrism to pass when prism extracts successfully")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when prism cannot extract", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
result := PrismTestFailure{Error: "something went wrong"}
|
||||
isSuccess := FromPrism(successPrism)
|
||||
testResult := isSuccess(result)(mockT)
|
||||
if testResult {
|
||||
t.Error("Expected FromPrism to fail when prism cannot extract")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with failure prism", func(t *testing.T) {
|
||||
result := PrismTestFailure{Error: "test error"}
|
||||
isFailure := FromPrism(failurePrism)
|
||||
testResult := isFailure(result)(t)
|
||||
if !testResult {
|
||||
t.Error("Expected FromPrism to pass for failure prism on failure result")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail with failure prism on success result", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
result := PrismTestSuccess{Value: 100}
|
||||
isFailure := FromPrism(failurePrism)
|
||||
testResult := isFailure(result)(mockT)
|
||||
if testResult {
|
||||
t.Error("Expected FromPrism to fail for failure prism on success result")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
package assert
|
||||
|
||||
import "github.com/IBM/fp-go/v2/result"
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/optics/optional"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
type (
|
||||
Result[T any] = result.Result[T]
|
||||
Result[T any] = result.Result[T]
|
||||
Reader = reader.Reader[*testing.T, bool]
|
||||
Kleisli[T any] = reader.Reader[T, Reader]
|
||||
Predicate[T any] = predicate.Predicate[T]
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
Optional[S, T any] = optional.Optional[S, T]
|
||||
Prism[S, T any] = prism.Prism[S, T]
|
||||
)
|
||||
|
||||
@@ -8,5 +8,5 @@ import (
|
||||
|
||||
// BuilderPrism createa a [Prism] that converts between a builder and its type
|
||||
func BuilderPrism[T any, B Builder[T]](creator func(T) B) Prism[B, T] {
|
||||
return prism.MakePrism(F.Flow2(B.Build, result.ToOption[T]), creator)
|
||||
return prism.MakePrismWithName(F.Flow2(B.Build, result.ToOption[T]), creator, "BuilderPrism")
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@ import (
|
||||
|
||||
// resourceState tracks the lifecycle of resources for testing
|
||||
type resourceState struct {
|
||||
resourcesCreated int
|
||||
resourcesCreated int
|
||||
resourcesReleased int
|
||||
lastError error
|
||||
lastError error
|
||||
}
|
||||
|
||||
// mockResource represents a test resource
|
||||
|
||||
@@ -68,7 +68,7 @@ func Of[S, A any](a A) StateReaderIOResult[S, A] {
|
||||
//
|
||||
// result := statereaderioresult.MonadMap(
|
||||
// statereaderioresult.Of[AppState](21),
|
||||
// func(x int) int { return x * 2 },
|
||||
// N.Mul(2),
|
||||
// ) // Result contains 42
|
||||
func MonadMap[S, A, B any](fa StateReaderIOResult[S, A], f func(A) B) StateReaderIOResult[S, B] {
|
||||
return statet.MonadMap[StateReaderIOResult[S, A], StateReaderIOResult[S, B]](
|
||||
@@ -83,7 +83,7 @@ func MonadMap[S, A, B any](fa StateReaderIOResult[S, A], f func(A) B) StateReade
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// double := statereaderioresult.Map[AppState](func(x int) int { return x * 2 })
|
||||
// double := statereaderioresult.Map[AppState](N.Mul(2))
|
||||
// result := function.Pipe1(statereaderioresult.Of[AppState](21), double)
|
||||
func Map[S, A, B any](f func(A) B) Operator[S, A, B] {
|
||||
return statet.Map[StateReaderIOResult[S, A], StateReaderIOResult[S, B]](
|
||||
@@ -135,7 +135,7 @@ func Chain[S, A, B any](f Kleisli[S, A, B]) Operator[S, A, B] {
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// fab := statereaderioresult.Of[AppState](func(x int) int { return x * 2 })
|
||||
// fab := statereaderioresult.Of[AppState](N.Mul(2))
|
||||
// fa := statereaderioresult.Of[AppState](21)
|
||||
// result := statereaderioresult.MonadAp(fab, fa) // Result contains 42
|
||||
func MonadAp[B, S, A any](fab StateReaderIOResult[S, func(A) B], fa StateReaderIOResult[S, A]) StateReaderIOResult[S, B] {
|
||||
|
||||
@@ -215,7 +215,7 @@ func TestFromState(t *testing.T) {
|
||||
|
||||
assert.True(t, RES.IsRight(res))
|
||||
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
|
||||
assert.Equal(t, 11, P.Tail(p)) // Incremented value
|
||||
assert.Equal(t, 11, P.Tail(p)) // Incremented value
|
||||
assert.Equal(t, 11, P.Head(p).counter) // State updated
|
||||
return p
|
||||
})(res)
|
||||
@@ -473,7 +473,7 @@ func TestStatefulComputation(t *testing.T) {
|
||||
res := result(initialState)(ctx)()
|
||||
assert.True(t, RES.IsRight(res))
|
||||
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
|
||||
assert.Equal(t, 3, P.Tail(p)) // Last incremented value
|
||||
assert.Equal(t, 3, P.Tail(p)) // Last incremented value
|
||||
assert.Equal(t, 3, P.Head(p).counter) // State updated three times
|
||||
return p
|
||||
})(res)
|
||||
|
||||
@@ -19,12 +19,12 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
RIORES "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
ST "github.com/IBM/fp-go/v2/context/statereaderioresult"
|
||||
EQ "github.com/IBM/fp-go/v2/eq"
|
||||
L "github.com/IBM/fp-go/v2/internal/monad/testing"
|
||||
P "github.com/IBM/fp-go/v2/pair"
|
||||
RES "github.com/IBM/fp-go/v2/result"
|
||||
RIORES "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
ST "github.com/IBM/fp-go/v2/context/statereaderioresult"
|
||||
)
|
||||
|
||||
// AssertLaws asserts the monad laws for the StateReaderIOResult monad
|
||||
|
||||
@@ -95,11 +95,11 @@ func (o *eitherMonad[E, A, B]) Chain(f Kleisli[E, A, B]) Operator[E, A, B] {
|
||||
// m := either.Monad[error, int, int]()
|
||||
//
|
||||
// // Map transforms the value
|
||||
// value := m.Map(func(x int) int { return x * 2 })(either.Right[error](21))
|
||||
// value := m.Map(N.Mul(2))(either.Right[error](21))
|
||||
// // value is Right(42)
|
||||
//
|
||||
// // Ap applies wrapped functions (also fails fast)
|
||||
// fn := either.Right[error](func(x int) int { return x + 1 })
|
||||
// fn := either.Right[error](N.Add(1))
|
||||
// result := m.Ap(value)(fn)
|
||||
// // result is Right(43)
|
||||
//
|
||||
|
||||
406
v2/endomorphism/builder.go
Normal file
406
v2/endomorphism/builder.go
Normal file
@@ -0,0 +1,406 @@
|
||||
// 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 (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
A "github.com/IBM/fp-go/v2/internal/array"
|
||||
)
|
||||
|
||||
// Build applies an endomorphism to the zero value of type A, effectively using
|
||||
// the endomorphism as a builder pattern.
|
||||
//
|
||||
// # Endomorphism as Builder Pattern
|
||||
//
|
||||
// An endomorphism (a function from type A to type A) can be viewed as a builder pattern
|
||||
// because it transforms a value of a type into another value of the same type. When you
|
||||
// compose multiple endomorphisms together, you create a pipeline of transformations that
|
||||
// build up a final value step by step.
|
||||
//
|
||||
// The Build function starts with the zero value of type A and applies the endomorphism
|
||||
// to it, making it particularly useful for building complex values from scratch using
|
||||
// a functional composition of transformations.
|
||||
//
|
||||
// # Builder Pattern Characteristics
|
||||
//
|
||||
// Traditional builder patterns have these characteristics:
|
||||
// 1. Start with an initial (often empty) state
|
||||
// 2. Apply a series of transformations/configurations
|
||||
// 3. Return the final built object
|
||||
//
|
||||
// Endomorphisms provide the same pattern functionally:
|
||||
// 1. Start with zero value: var a A
|
||||
// 2. Apply composed endomorphisms: e(a)
|
||||
// 3. Return the transformed value
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The type being built/transformed
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - e: An endomorphism (or composition of endomorphisms) that transforms type A
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// The result of applying the endomorphism to the zero value of type A
|
||||
//
|
||||
// # Example - Building a Configuration Object
|
||||
//
|
||||
// type Config struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// Timeout time.Duration
|
||||
// Debug bool
|
||||
// }
|
||||
//
|
||||
// // Define builder functions as endomorphisms
|
||||
// withHost := func(host string) Endomorphism[Config] {
|
||||
// return func(c Config) Config {
|
||||
// c.Host = host
|
||||
// return c
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// withPort := func(port int) Endomorphism[Config] {
|
||||
// return func(c Config) Config {
|
||||
// c.Port = port
|
||||
// return c
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// withTimeout := func(d time.Duration) Endomorphism[Config] {
|
||||
// return func(c Config) Config {
|
||||
// c.Timeout = d
|
||||
// return c
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// withDebug := func(debug bool) Endomorphism[Config] {
|
||||
// return func(c Config) Config {
|
||||
// c.Debug = debug
|
||||
// return c
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Compose builders using monoid operations
|
||||
// import M "github.com/IBM/fp-go/v2/monoid"
|
||||
//
|
||||
// configBuilder := M.ConcatAll(Monoid[Config]())(
|
||||
// withHost("localhost"),
|
||||
// withPort(8080),
|
||||
// withTimeout(30 * time.Second),
|
||||
// withDebug(true),
|
||||
// )
|
||||
//
|
||||
// // Build the final configuration
|
||||
// config := Build(configBuilder)
|
||||
// // Result: Config{Host: "localhost", Port: 8080, Timeout: 30s, Debug: true}
|
||||
//
|
||||
// # Example - Building a String with Transformations
|
||||
//
|
||||
// import (
|
||||
// "strings"
|
||||
// M "github.com/IBM/fp-go/v2/monoid"
|
||||
// )
|
||||
//
|
||||
// // Define string transformation endomorphisms
|
||||
// appendHello := func(s string) string { return s + "Hello" }
|
||||
// appendSpace := func(s string) string { return s + " " }
|
||||
// appendWorld := func(s string) string { return s + "World" }
|
||||
// toUpper := strings.ToUpper
|
||||
//
|
||||
// // Compose transformations
|
||||
// stringBuilder := M.ConcatAll(Monoid[string]())(
|
||||
// appendHello,
|
||||
// appendSpace,
|
||||
// appendWorld,
|
||||
// toUpper,
|
||||
// )
|
||||
//
|
||||
// // Build the final string from empty string
|
||||
// result := Build(stringBuilder)
|
||||
// // Result: "HELLO WORLD"
|
||||
//
|
||||
// # Example - Building a Slice with Operations
|
||||
//
|
||||
// type IntSlice []int
|
||||
//
|
||||
// appendValue := func(v int) Endomorphism[IntSlice] {
|
||||
// return func(s IntSlice) IntSlice {
|
||||
// return append(s, v)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// sortSlice := func(s IntSlice) IntSlice {
|
||||
// sorted := make(IntSlice, len(s))
|
||||
// copy(sorted, s)
|
||||
// sort.Ints(sorted)
|
||||
// return sorted
|
||||
// }
|
||||
//
|
||||
// // Build a sorted slice
|
||||
// sliceBuilder := M.ConcatAll(Monoid[IntSlice]())(
|
||||
// appendValue(5),
|
||||
// appendValue(2),
|
||||
// appendValue(8),
|
||||
// appendValue(1),
|
||||
// sortSlice,
|
||||
// )
|
||||
//
|
||||
// result := Build(sliceBuilder)
|
||||
// // Result: IntSlice{1, 2, 5, 8}
|
||||
//
|
||||
// # Advantages of Endomorphism Builder Pattern
|
||||
//
|
||||
// 1. **Composability**: Builders can be composed using monoid operations
|
||||
// 2. **Immutability**: Each transformation returns a new value (if implemented immutably)
|
||||
// 3. **Type Safety**: The type system ensures all transformations work on the same type
|
||||
// 4. **Reusability**: Individual builder functions can be reused and combined differently
|
||||
// 5. **Testability**: Each transformation can be tested independently
|
||||
// 6. **Declarative**: The composition clearly expresses the building process
|
||||
//
|
||||
// # Comparison with Traditional Builder Pattern
|
||||
//
|
||||
// Traditional OOP Builder:
|
||||
//
|
||||
// config := NewConfigBuilder().
|
||||
// WithHost("localhost").
|
||||
// WithPort(8080).
|
||||
// WithTimeout(30 * time.Second).
|
||||
// Build()
|
||||
//
|
||||
// Endomorphism Builder:
|
||||
//
|
||||
// config := Build(M.ConcatAll(Monoid[Config]())(
|
||||
// withHost("localhost"),
|
||||
// withPort(8080),
|
||||
// withTimeout(30 * time.Second),
|
||||
// ))
|
||||
//
|
||||
// Both achieve the same goal, but the endomorphism approach:
|
||||
// - Uses pure functions instead of methods
|
||||
// - Leverages algebraic properties (monoid) for composition
|
||||
// - Allows for more flexible composition patterns
|
||||
// - Integrates naturally with other functional programming constructs
|
||||
func Build[A any](e Endomorphism[A]) A {
|
||||
var a A
|
||||
return e(a)
|
||||
}
|
||||
|
||||
// ConcatAll combines multiple endomorphisms into a single endomorphism using composition.
|
||||
//
|
||||
// This function takes a slice of endomorphisms and combines them using the monoid's
|
||||
// concat operation (which is composition). The resulting endomorphism, when applied,
|
||||
// will execute all the input endomorphisms in RIGHT-TO-LEFT order (mathematical composition order).
|
||||
//
|
||||
// IMPORTANT: Execution order is RIGHT-TO-LEFT:
|
||||
// - ConcatAll([]Endomorphism{f, g, h}) creates an endomorphism that applies h, then g, then f
|
||||
// - This is equivalent to f ∘ g ∘ h in mathematical notation
|
||||
// - The last endomorphism in the slice is applied first
|
||||
//
|
||||
// If the slice is empty, returns the identity endomorphism.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - T: The type that the endomorphisms operate on
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - es: A slice of endomorphisms to combine
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A single endomorphism that represents the composition of all input endomorphisms
|
||||
//
|
||||
// # Example - Basic Composition
|
||||
//
|
||||
// double := N.Mul(2)
|
||||
// increment := N.Add(1)
|
||||
// square := func(x int) int { return x * x }
|
||||
//
|
||||
// // Combine endomorphisms (RIGHT-TO-LEFT execution)
|
||||
// combined := ConcatAll([]Endomorphism[int]{double, increment, square})
|
||||
// result := combined(5)
|
||||
// // Execution: square(5) = 25, increment(25) = 26, double(26) = 52
|
||||
// // Result: 52
|
||||
//
|
||||
// # Example - Building with ConcatAll
|
||||
//
|
||||
// type Config struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
//
|
||||
// withHost := func(host string) Endomorphism[Config] {
|
||||
// return func(c Config) Config {
|
||||
// c.Host = host
|
||||
// return c
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// withPort := func(port int) Endomorphism[Config] {
|
||||
// return func(c Config) Config {
|
||||
// c.Port = port
|
||||
// return c
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Combine configuration builders
|
||||
// configBuilder := ConcatAll([]Endomorphism[Config]{
|
||||
// withHost("localhost"),
|
||||
// withPort(8080),
|
||||
// })
|
||||
//
|
||||
// // Apply to zero value
|
||||
// config := Build(configBuilder)
|
||||
// // Result: Config{Host: "localhost", Port: 8080}
|
||||
//
|
||||
// # Example - Empty Slice
|
||||
//
|
||||
// // Empty slice returns identity
|
||||
// identity := ConcatAll([]Endomorphism[int]{})
|
||||
// result := identity(42) // Returns: 42
|
||||
//
|
||||
// # Relationship to Monoid
|
||||
//
|
||||
// ConcatAll is equivalent to using M.ConcatAll with the endomorphism Monoid:
|
||||
//
|
||||
// import M "github.com/IBM/fp-go/v2/monoid"
|
||||
//
|
||||
// // These are equivalent:
|
||||
// result1 := ConcatAll(endomorphisms)
|
||||
// result2 := M.ConcatAll(Monoid[T]())(endomorphisms)
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// 1. **Pipeline Construction**: Build transformation pipelines from individual steps
|
||||
// 2. **Configuration Building**: Combine multiple configuration setters
|
||||
// 3. **Data Transformation**: Chain multiple data transformations
|
||||
// 4. **Middleware Composition**: Combine middleware functions
|
||||
// 5. **Validation Chains**: Compose multiple validation functions
|
||||
func ConcatAll[T any](es []Endomorphism[T]) Endomorphism[T] {
|
||||
return A.Reduce(es, MonadCompose[T], function.Identity[T])
|
||||
}
|
||||
|
||||
// Reduce applies a slice of endomorphisms to the zero value of type T in LEFT-TO-RIGHT order.
|
||||
//
|
||||
// This function is a convenience wrapper that:
|
||||
// 1. Starts with the zero value of type T
|
||||
// 2. Applies each endomorphism in the slice from left to right
|
||||
// 3. Returns the final transformed value
|
||||
//
|
||||
// IMPORTANT: Execution order is LEFT-TO-RIGHT:
|
||||
// - Reduce([]Endomorphism{f, g, h}) applies f first, then g, then h
|
||||
// - This is the opposite of ConcatAll's RIGHT-TO-LEFT order
|
||||
// - Each endomorphism receives the result of the previous one
|
||||
//
|
||||
// This is equivalent to: Build(ConcatAll(reverse(es))) but more efficient and clearer
|
||||
// for left-to-right sequential application.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - T: The type being transformed
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - es: A slice of endomorphisms to apply sequentially
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// The final value after applying all endomorphisms to the zero value
|
||||
//
|
||||
// # Example - Sequential Transformations
|
||||
//
|
||||
// double := N.Mul(2)
|
||||
// increment := N.Add(1)
|
||||
// square := func(x int) int { return x * x }
|
||||
//
|
||||
// // Apply transformations LEFT-TO-RIGHT
|
||||
// result := Reduce([]Endomorphism[int]{double, increment, square})
|
||||
// // Execution: 0 -> double(0) = 0 -> increment(0) = 1 -> square(1) = 1
|
||||
// // Result: 1
|
||||
//
|
||||
// // With a non-zero starting point, use a custom initial value:
|
||||
// addTen := N.Add(10)
|
||||
// result2 := Reduce([]Endomorphism[int]{addTen, double, increment})
|
||||
// // Execution: 0 -> addTen(0) = 10 -> double(10) = 20 -> increment(20) = 21
|
||||
// // Result: 21
|
||||
//
|
||||
// # Example - Building a String
|
||||
//
|
||||
// appendHello := func(s string) string { return s + "Hello" }
|
||||
// appendSpace := func(s string) string { return s + " " }
|
||||
// appendWorld := func(s string) string { return s + "World" }
|
||||
//
|
||||
// // Build string LEFT-TO-RIGHT
|
||||
// result := Reduce([]Endomorphism[string]{
|
||||
// appendHello,
|
||||
// appendSpace,
|
||||
// appendWorld,
|
||||
// })
|
||||
// // Execution: "" -> "Hello" -> "Hello " -> "Hello World"
|
||||
// // Result: "Hello World"
|
||||
//
|
||||
// # Example - Configuration Building
|
||||
//
|
||||
// type Settings struct {
|
||||
// Theme string
|
||||
// FontSize int
|
||||
// }
|
||||
//
|
||||
// withTheme := func(theme string) Endomorphism[Settings] {
|
||||
// return func(s Settings) Settings {
|
||||
// s.Theme = theme
|
||||
// return s
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// withFontSize := func(size int) Endomorphism[Settings] {
|
||||
// return func(s Settings) Settings {
|
||||
// s.FontSize = size
|
||||
// return s
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Build settings LEFT-TO-RIGHT
|
||||
// settings := Reduce([]Endomorphism[Settings]{
|
||||
// withTheme("dark"),
|
||||
// withFontSize(14),
|
||||
// })
|
||||
// // Result: Settings{Theme: "dark", FontSize: 14}
|
||||
//
|
||||
// # Comparison with ConcatAll
|
||||
//
|
||||
// // ConcatAll: RIGHT-TO-LEFT composition, returns endomorphism
|
||||
// endo := ConcatAll([]Endomorphism[int]{f, g, h})
|
||||
// result1 := endo(value) // Applies h, then g, then f
|
||||
//
|
||||
// // Reduce: LEFT-TO-RIGHT application, returns final value
|
||||
// result2 := Reduce([]Endomorphism[int]{f, g, h})
|
||||
// // Applies f to zero, then g, then h
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// 1. **Sequential Processing**: Apply transformations in order
|
||||
// 2. **Pipeline Execution**: Execute a pipeline from start to finish
|
||||
// 3. **Builder Pattern**: Build objects step by step
|
||||
// 4. **State Machines**: Apply state transitions in sequence
|
||||
// 5. **Data Flow**: Transform data through multiple stages
|
||||
func Reduce[T any](es []Endomorphism[T]) T {
|
||||
var t T
|
||||
return A.Reduce(es, func(t T, e Endomorphism[T]) T { return e(t) }, t)
|
||||
}
|
||||
254
v2/endomorphism/builder_example_test.go
Normal file
254
v2/endomorphism/builder_example_test.go
Normal file
@@ -0,0 +1,254 @@
|
||||
// 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_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
// Example_build_basicUsage demonstrates basic usage of the Build function
|
||||
// to construct a value from the zero value using endomorphisms.
|
||||
func Example_build_basicUsage() {
|
||||
// Define simple endomorphisms
|
||||
addTen := N.Add(10)
|
||||
double := N.Mul(2)
|
||||
|
||||
// Compose them using monoid (RIGHT-TO-LEFT execution)
|
||||
// double is applied first, then addTen
|
||||
builder := M.ConcatAll(endomorphism.Monoid[int]())(A.From(
|
||||
addTen,
|
||||
double,
|
||||
))
|
||||
|
||||
// Build from zero value: 0 * 2 = 0, 0 + 10 = 10
|
||||
result := endomorphism.Build(builder)
|
||||
fmt.Println(result)
|
||||
// Output: 10
|
||||
}
|
||||
|
||||
// Example_build_configBuilder demonstrates using Build as a configuration builder pattern.
|
||||
func Example_build_configBuilder() {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
Timeout time.Duration
|
||||
Debug bool
|
||||
}
|
||||
|
||||
// Define builder functions as endomorphisms
|
||||
withHost := func(host string) endomorphism.Endomorphism[Config] {
|
||||
return func(c Config) Config {
|
||||
c.Host = host
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
withPort := func(port int) endomorphism.Endomorphism[Config] {
|
||||
return func(c Config) Config {
|
||||
c.Port = port
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
withTimeout := func(d time.Duration) endomorphism.Endomorphism[Config] {
|
||||
return func(c Config) Config {
|
||||
c.Timeout = d
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
withDebug := func(debug bool) endomorphism.Endomorphism[Config] {
|
||||
return func(c Config) Config {
|
||||
c.Debug = debug
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
// Compose builders using monoid
|
||||
configBuilder := M.ConcatAll(endomorphism.Monoid[Config]())([]endomorphism.Endomorphism[Config]{
|
||||
withHost("localhost"),
|
||||
withPort(8080),
|
||||
withTimeout(30 * time.Second),
|
||||
withDebug(true),
|
||||
})
|
||||
|
||||
// Build the configuration from zero value
|
||||
config := endomorphism.Build(configBuilder)
|
||||
|
||||
fmt.Printf("Host: %s\n", config.Host)
|
||||
fmt.Printf("Port: %d\n", config.Port)
|
||||
fmt.Printf("Timeout: %v\n", config.Timeout)
|
||||
fmt.Printf("Debug: %v\n", config.Debug)
|
||||
// Output:
|
||||
// Host: localhost
|
||||
// Port: 8080
|
||||
// Timeout: 30s
|
||||
// Debug: true
|
||||
}
|
||||
|
||||
// Example_build_stringBuilder demonstrates building a string using endomorphisms.
|
||||
func Example_build_stringBuilder() {
|
||||
// Define string transformation endomorphisms
|
||||
appendHello := func(s string) string { return s + "Hello" }
|
||||
appendSpace := func(s string) string { return s + " " }
|
||||
appendWorld := func(s string) string { return s + "World" }
|
||||
appendExclamation := func(s string) string { return s + "!" }
|
||||
|
||||
// Compose transformations (RIGHT-TO-LEFT execution)
|
||||
stringBuilder := M.ConcatAll(endomorphism.Monoid[string]())([]endomorphism.Endomorphism[string]{
|
||||
appendHello,
|
||||
appendSpace,
|
||||
appendWorld,
|
||||
appendExclamation,
|
||||
})
|
||||
|
||||
// Build the string from empty string
|
||||
result := endomorphism.Build(stringBuilder)
|
||||
fmt.Println(result)
|
||||
// Output: !World Hello
|
||||
}
|
||||
|
||||
// Example_build_personBuilder demonstrates building a complex struct using the builder pattern.
|
||||
func Example_build_personBuilder() {
|
||||
type Person struct {
|
||||
FirstName string
|
||||
LastName string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
// Define builder functions
|
||||
withFirstName := func(name string) endomorphism.Endomorphism[Person] {
|
||||
return func(p Person) Person {
|
||||
p.FirstName = name
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
withLastName := func(name string) endomorphism.Endomorphism[Person] {
|
||||
return func(p Person) Person {
|
||||
p.LastName = name
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
withAge := func(age int) endomorphism.Endomorphism[Person] {
|
||||
return func(p Person) Person {
|
||||
p.Age = age
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
withEmail := func(email string) endomorphism.Endomorphism[Person] {
|
||||
return func(p Person) Person {
|
||||
p.Email = email
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
// Build a person
|
||||
personBuilder := M.ConcatAll(endomorphism.Monoid[Person]())([]endomorphism.Endomorphism[Person]{
|
||||
withFirstName("Alice"),
|
||||
withLastName("Smith"),
|
||||
withAge(30),
|
||||
withEmail("alice.smith@example.com"),
|
||||
})
|
||||
|
||||
person := endomorphism.Build(personBuilder)
|
||||
|
||||
fmt.Printf("%s %s, Age: %d, Email: %s\n",
|
||||
person.FirstName, person.LastName, person.Age, person.Email)
|
||||
// Output: Alice Smith, Age: 30, Email: alice.smith@example.com
|
||||
}
|
||||
|
||||
// Example_build_conditionalBuilder demonstrates conditional building using endomorphisms.
|
||||
func Example_build_conditionalBuilder() {
|
||||
type Settings struct {
|
||||
Theme string
|
||||
FontSize int
|
||||
AutoSave bool
|
||||
Animations bool
|
||||
}
|
||||
|
||||
withTheme := func(theme string) endomorphism.Endomorphism[Settings] {
|
||||
return func(s Settings) Settings {
|
||||
s.Theme = theme
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
withFontSize := func(size int) endomorphism.Endomorphism[Settings] {
|
||||
return func(s Settings) Settings {
|
||||
s.FontSize = size
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
withAutoSave := func(enabled bool) endomorphism.Endomorphism[Settings] {
|
||||
return func(s Settings) Settings {
|
||||
s.AutoSave = enabled
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
withAnimations := func(enabled bool) endomorphism.Endomorphism[Settings] {
|
||||
return func(s Settings) Settings {
|
||||
s.Animations = enabled
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
// Build settings conditionally
|
||||
isDarkMode := true
|
||||
isAccessibilityMode := true
|
||||
|
||||
// Note: Monoid executes RIGHT-TO-LEFT, so later items in the slice are applied first
|
||||
// We need to add items in reverse order for the desired effect
|
||||
builders := []endomorphism.Endomorphism[Settings]{}
|
||||
|
||||
if isAccessibilityMode {
|
||||
builders = append(builders, withFontSize(18)) // Will be applied last (overrides)
|
||||
builders = append(builders, withAnimations(false))
|
||||
}
|
||||
|
||||
if isDarkMode {
|
||||
builders = append(builders, withTheme("dark"))
|
||||
} else {
|
||||
builders = append(builders, withTheme("light"))
|
||||
}
|
||||
|
||||
builders = append(builders, withAutoSave(true))
|
||||
builders = append(builders, withFontSize(14)) // Will be applied first
|
||||
|
||||
settingsBuilder := M.ConcatAll(endomorphism.Monoid[Settings]())(builders)
|
||||
settings := endomorphism.Build(settingsBuilder)
|
||||
|
||||
fmt.Printf("Theme: %s\n", settings.Theme)
|
||||
fmt.Printf("FontSize: %d\n", settings.FontSize)
|
||||
fmt.Printf("AutoSave: %v\n", settings.AutoSave)
|
||||
fmt.Printf("Animations: %v\n", settings.Animations)
|
||||
// Output:
|
||||
// Theme: dark
|
||||
// FontSize: 18
|
||||
// AutoSave: true
|
||||
// Animations: false
|
||||
}
|
||||
@@ -37,7 +37,7 @@
|
||||
//
|
||||
// // Define some endomorphisms
|
||||
// double := N.Mul(2)
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// increment := N.Add(1)
|
||||
//
|
||||
// // Compose them (RIGHT-TO-LEFT execution)
|
||||
// composed := endomorphism.Compose(double, increment)
|
||||
@@ -63,8 +63,8 @@
|
||||
// // Combine multiple endomorphisms (RIGHT-TO-LEFT execution)
|
||||
// combined := M.ConcatAll(monoid)(
|
||||
// N.Mul(2), // applied third
|
||||
// func(x int) int { return x + 1 }, // applied second
|
||||
// func(x int) int { return x * 3 }, // applied first
|
||||
// N.Add(1), // applied second
|
||||
// N.Mul(3), // applied first
|
||||
// )
|
||||
// result := combined(5) // (5 * 3) = 15, (15 + 1) = 16, (16 * 2) = 32
|
||||
//
|
||||
@@ -75,7 +75,7 @@
|
||||
//
|
||||
// // Chain allows sequencing of endomorphisms (LEFT-TO-RIGHT)
|
||||
// f := N.Mul(2)
|
||||
// g := func(x int) int { return x + 1 }
|
||||
// g := N.Add(1)
|
||||
// chained := endomorphism.MonadChain(f, g) // f first, then g
|
||||
// result := chained(5) // (5 * 2) + 1 = 11
|
||||
//
|
||||
@@ -84,7 +84,7 @@
|
||||
// The key difference between Compose and Chain/MonadChain is execution order:
|
||||
//
|
||||
// double := N.Mul(2)
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// increment := N.Add(1)
|
||||
//
|
||||
// // Compose: RIGHT-TO-LEFT (mathematical composition)
|
||||
// composed := endomorphism.Compose(double, increment)
|
||||
|
||||
@@ -38,7 +38,7 @@ import (
|
||||
// Example:
|
||||
//
|
||||
// double := N.Mul(2)
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// increment := N.Add(1)
|
||||
// result := endomorphism.MonadAp(double, increment) // Composes: double ∘ increment
|
||||
// // result(5) = double(increment(5)) = double(6) = 12
|
||||
func MonadAp[A any](fab Endomorphism[A], fa Endomorphism[A]) Endomorphism[A] {
|
||||
@@ -62,7 +62,7 @@ func MonadAp[A any](fab Endomorphism[A], fa Endomorphism[A]) Endomorphism[A] {
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// increment := N.Add(1)
|
||||
// applyIncrement := endomorphism.Ap(increment)
|
||||
// double := N.Mul(2)
|
||||
// composed := applyIncrement(double) // double ∘ increment
|
||||
@@ -92,7 +92,7 @@ func Ap[A any](fa Endomorphism[A]) Operator[A] {
|
||||
// Example:
|
||||
//
|
||||
// double := N.Mul(2)
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// increment := N.Add(1)
|
||||
//
|
||||
// // MonadCompose executes RIGHT-TO-LEFT: increment first, then double
|
||||
// composed := endomorphism.MonadCompose(double, increment)
|
||||
@@ -124,7 +124,7 @@ func MonadCompose[A any](f, g Endomorphism[A]) Endomorphism[A] {
|
||||
// Example:
|
||||
//
|
||||
// double := N.Mul(2)
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// increment := N.Add(1)
|
||||
// mapped := endomorphism.MonadMap(double, increment)
|
||||
// // mapped(5) = double(increment(5)) = double(6) = 12
|
||||
func MonadMap[A any](f, g Endomorphism[A]) Endomorphism[A] {
|
||||
@@ -151,7 +151,7 @@ func MonadMap[A any](f, g Endomorphism[A]) Endomorphism[A] {
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// increment := N.Add(1)
|
||||
// composeWithIncrement := endomorphism.Compose(increment)
|
||||
// double := N.Mul(2)
|
||||
//
|
||||
@@ -188,7 +188,7 @@ func Compose[A any](g Endomorphism[A]) Operator[A] {
|
||||
//
|
||||
// double := N.Mul(2)
|
||||
// mapDouble := endomorphism.Map(double)
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// increment := N.Add(1)
|
||||
// mapped := mapDouble(increment)
|
||||
// // mapped(5) = double(increment(5)) = double(6) = 12
|
||||
func Map[A any](f Endomorphism[A]) Operator[A] {
|
||||
@@ -216,7 +216,7 @@ func Map[A any](f Endomorphism[A]) Operator[A] {
|
||||
// Example:
|
||||
//
|
||||
// double := N.Mul(2)
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// increment := N.Add(1)
|
||||
//
|
||||
// // MonadChain executes LEFT-TO-RIGHT: double first, then increment
|
||||
// chained := endomorphism.MonadChain(double, increment)
|
||||
@@ -294,7 +294,7 @@ func ChainFirst[A any](f Endomorphism[A]) Operator[A] {
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// increment := N.Add(1)
|
||||
// chainWithIncrement := endomorphism.Chain(increment)
|
||||
// double := N.Mul(2)
|
||||
//
|
||||
|
||||
@@ -206,7 +206,7 @@ func TestCompose(t *testing.T) {
|
||||
// TestMonadComposeVsCompose demonstrates the relationship between MonadCompose and Compose
|
||||
func TestMonadComposeVsCompose(t *testing.T) {
|
||||
double := N.Mul(2)
|
||||
increment := func(x int) int { return x + 1 }
|
||||
increment := N.Add(1)
|
||||
|
||||
// MonadCompose takes both functions at once
|
||||
monadComposed := MonadCompose(double, increment)
|
||||
@@ -458,7 +458,7 @@ func BenchmarkCompose(b *testing.B) {
|
||||
// TestComposeVsChain demonstrates the key difference between Compose and Chain
|
||||
func TestComposeVsChain(t *testing.T) {
|
||||
double := N.Mul(2)
|
||||
increment := func(x int) int { return x + 1 }
|
||||
increment := N.Add(1)
|
||||
|
||||
// Compose executes RIGHT-TO-LEFT
|
||||
// Compose(double, increment) means: increment first, then double
|
||||
@@ -722,3 +722,352 @@ func TestChainFirst(t *testing.T) {
|
||||
// But side effect should have been executed with double's result
|
||||
assert.Equal(t, 10, sideEffect, "ChainFirst should execute second function for effect")
|
||||
}
|
||||
|
||||
// TestBuild tests the Build function
|
||||
func TestBuild(t *testing.T) {
|
||||
t.Run("build with single transformation", func(t *testing.T) {
|
||||
// Build applies endomorphism to zero value
|
||||
result := Build(double)
|
||||
assert.Equal(t, 0, result, "Build(double) on zero value should be 0")
|
||||
})
|
||||
|
||||
t.Run("build with composed transformations", func(t *testing.T) {
|
||||
// Create a builder that starts from zero and applies transformations
|
||||
builder := M.ConcatAll(Monoid[int]())([]Endomorphism[int]{
|
||||
N.Add(10),
|
||||
N.Mul(2),
|
||||
N.Add(5),
|
||||
})
|
||||
|
||||
result := Build(builder)
|
||||
// RIGHT-TO-LEFT: 0 + 5 = 5, 5 * 2 = 10, 10 + 10 = 20
|
||||
assert.Equal(t, 20, result, "Build should apply composed transformations to zero value")
|
||||
})
|
||||
|
||||
t.Run("build with identity", func(t *testing.T) {
|
||||
result := Build(Identity[int]())
|
||||
assert.Equal(t, 0, result, "Build(identity) should return zero value")
|
||||
})
|
||||
|
||||
t.Run("build string from empty", func(t *testing.T) {
|
||||
builder := M.ConcatAll(Monoid[string]())([]Endomorphism[string]{
|
||||
func(s string) string { return s + "Hello" },
|
||||
func(s string) string { return s + " " },
|
||||
func(s string) string { return s + "World" },
|
||||
})
|
||||
|
||||
result := Build(builder)
|
||||
// RIGHT-TO-LEFT: "" + "World" = "World", "World" + " " = "World ", "World " + "Hello" = "World Hello"
|
||||
assert.Equal(t, "World Hello", result, "Build should work with strings")
|
||||
})
|
||||
|
||||
t.Run("build struct with builder pattern", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
withHost := func(host string) Endomorphism[Config] {
|
||||
return func(c Config) Config {
|
||||
c.Host = host
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
withPort := func(port int) Endomorphism[Config] {
|
||||
return func(c Config) Config {
|
||||
c.Port = port
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
builder := M.ConcatAll(Monoid[Config]())([]Endomorphism[Config]{
|
||||
withHost("localhost"),
|
||||
withPort(8080),
|
||||
})
|
||||
|
||||
result := Build(builder)
|
||||
assert.Equal(t, "localhost", result.Host, "Build should set Host")
|
||||
assert.Equal(t, 8080, result.Port, "Build should set Port")
|
||||
})
|
||||
|
||||
t.Run("build slice with operations", func(t *testing.T) {
|
||||
type IntSlice []int
|
||||
|
||||
appendValue := func(v int) Endomorphism[IntSlice] {
|
||||
return func(s IntSlice) IntSlice {
|
||||
return append(s, v)
|
||||
}
|
||||
}
|
||||
|
||||
builder := M.ConcatAll(Monoid[IntSlice]())([]Endomorphism[IntSlice]{
|
||||
appendValue(1),
|
||||
appendValue(2),
|
||||
appendValue(3),
|
||||
})
|
||||
|
||||
result := Build(builder)
|
||||
// RIGHT-TO-LEFT: append 3, append 2, append 1
|
||||
assert.Equal(t, IntSlice{3, 2, 1}, result, "Build should construct slice")
|
||||
})
|
||||
}
|
||||
|
||||
// TestBuildAsBuilderPattern demonstrates using Build as a builder pattern
|
||||
func TestBuildAsBuilderPattern(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
Active bool
|
||||
}
|
||||
|
||||
// Define builder functions
|
||||
withName := func(name string) Endomorphism[Person] {
|
||||
return func(p Person) Person {
|
||||
p.Name = name
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
withAge := func(age int) Endomorphism[Person] {
|
||||
return func(p Person) Person {
|
||||
p.Age = age
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
withEmail := func(email string) Endomorphism[Person] {
|
||||
return func(p Person) Person {
|
||||
p.Email = email
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
withActive := func(active bool) Endomorphism[Person] {
|
||||
return func(p Person) Person {
|
||||
p.Active = active
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
// Build a person using the builder pattern
|
||||
personBuilder := M.ConcatAll(Monoid[Person]())([]Endomorphism[Person]{
|
||||
withName("Alice"),
|
||||
withAge(30),
|
||||
withEmail("alice@example.com"),
|
||||
withActive(true),
|
||||
})
|
||||
|
||||
person := Build(personBuilder)
|
||||
|
||||
assert.Equal(t, "Alice", person.Name)
|
||||
assert.Equal(t, 30, person.Age)
|
||||
assert.Equal(t, "alice@example.com", person.Email)
|
||||
assert.True(t, person.Active)
|
||||
}
|
||||
|
||||
// TestConcatAll tests the ConcatAll function
|
||||
func TestConcatAll(t *testing.T) {
|
||||
t.Run("concat all with multiple endomorphisms", func(t *testing.T) {
|
||||
// ConcatAll executes RIGHT-TO-LEFT
|
||||
combined := ConcatAll([]Endomorphism[int]{double, increment, square})
|
||||
result := combined(5)
|
||||
// RIGHT-TO-LEFT: square(5) = 25, increment(25) = 26, double(26) = 52
|
||||
assert.Equal(t, 52, result, "ConcatAll should execute right-to-left")
|
||||
})
|
||||
|
||||
t.Run("concat all with empty slice", func(t *testing.T) {
|
||||
// Empty slice should return identity
|
||||
identity := ConcatAll([]Endomorphism[int]{})
|
||||
result := identity(42)
|
||||
assert.Equal(t, 42, result, "ConcatAll with empty slice should return identity")
|
||||
})
|
||||
|
||||
t.Run("concat all with single endomorphism", func(t *testing.T) {
|
||||
combined := ConcatAll([]Endomorphism[int]{double})
|
||||
result := combined(5)
|
||||
assert.Equal(t, 10, result, "ConcatAll with single endomorphism should apply it")
|
||||
})
|
||||
|
||||
t.Run("concat all with two endomorphisms", func(t *testing.T) {
|
||||
// RIGHT-TO-LEFT: increment first, then double
|
||||
combined := ConcatAll([]Endomorphism[int]{double, increment})
|
||||
result := combined(5)
|
||||
assert.Equal(t, 12, result, "ConcatAll should execute right-to-left: (5 + 1) * 2 = 12")
|
||||
})
|
||||
|
||||
t.Run("concat all with strings", func(t *testing.T) {
|
||||
appendHello := func(s string) string { return s + "Hello" }
|
||||
appendSpace := func(s string) string { return s + " " }
|
||||
appendWorld := func(s string) string { return s + "World" }
|
||||
|
||||
// RIGHT-TO-LEFT execution
|
||||
combined := ConcatAll([]Endomorphism[string]{appendHello, appendSpace, appendWorld})
|
||||
result := combined("")
|
||||
// RIGHT-TO-LEFT: "" + "World" = "World", "World" + " " = "World ", "World " + "Hello" = "World Hello"
|
||||
assert.Equal(t, "World Hello", result, "ConcatAll should work with strings")
|
||||
})
|
||||
|
||||
t.Run("concat all for building structs", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
withHost := func(host string) Endomorphism[Config] {
|
||||
return func(c Config) Config {
|
||||
c.Host = host
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
withPort := func(port int) Endomorphism[Config] {
|
||||
return func(c Config) Config {
|
||||
c.Port = port
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
combined := ConcatAll([]Endomorphism[Config]{
|
||||
withHost("localhost"),
|
||||
withPort(8080),
|
||||
})
|
||||
|
||||
result := combined(Config{})
|
||||
assert.Equal(t, "localhost", result.Host)
|
||||
assert.Equal(t, 8080, result.Port)
|
||||
})
|
||||
|
||||
t.Run("concat all is equivalent to monoid ConcatAll", func(t *testing.T) {
|
||||
endos := []Endomorphism[int]{double, increment, square}
|
||||
|
||||
result1 := ConcatAll(endos)(5)
|
||||
result2 := M.ConcatAll(Monoid[int]())(endos)(5)
|
||||
|
||||
assert.Equal(t, result1, result2, "ConcatAll should be equivalent to M.ConcatAll(Monoid())")
|
||||
})
|
||||
}
|
||||
|
||||
// TestReduce tests the Reduce function
|
||||
func TestReduce(t *testing.T) {
|
||||
t.Run("reduce with multiple endomorphisms", func(t *testing.T) {
|
||||
// Reduce executes LEFT-TO-RIGHT starting from zero value
|
||||
result := Reduce([]Endomorphism[int]{double, increment, square})
|
||||
// LEFT-TO-RIGHT: 0 -> double(0) = 0 -> increment(0) = 1 -> square(1) = 1
|
||||
assert.Equal(t, 1, result, "Reduce should execute left-to-right from zero value")
|
||||
})
|
||||
|
||||
t.Run("reduce with empty slice", func(t *testing.T) {
|
||||
// Empty slice should return zero value
|
||||
result := Reduce([]Endomorphism[int]{})
|
||||
assert.Equal(t, 0, result, "Reduce with empty slice should return zero value")
|
||||
})
|
||||
|
||||
t.Run("reduce with single endomorphism", func(t *testing.T) {
|
||||
addTen := N.Add(10)
|
||||
result := Reduce([]Endomorphism[int]{addTen})
|
||||
// 0 + 10 = 10
|
||||
assert.Equal(t, 10, result, "Reduce with single endomorphism should apply it to zero")
|
||||
})
|
||||
|
||||
t.Run("reduce with sequential transformations", func(t *testing.T) {
|
||||
addTen := N.Add(10)
|
||||
// LEFT-TO-RIGHT: 0 -> addTen(0) = 10 -> double(10) = 20 -> increment(20) = 21
|
||||
result := Reduce([]Endomorphism[int]{addTen, double, increment})
|
||||
assert.Equal(t, 21, result, "Reduce should apply transformations left-to-right")
|
||||
})
|
||||
|
||||
t.Run("reduce with strings", func(t *testing.T) {
|
||||
appendHello := func(s string) string { return s + "Hello" }
|
||||
appendSpace := func(s string) string { return s + " " }
|
||||
appendWorld := func(s string) string { return s + "World" }
|
||||
|
||||
// LEFT-TO-RIGHT execution
|
||||
result := Reduce([]Endomorphism[string]{appendHello, appendSpace, appendWorld})
|
||||
// "" -> "Hello" -> "Hello " -> "Hello World"
|
||||
assert.Equal(t, "Hello World", result, "Reduce should work with strings left-to-right")
|
||||
})
|
||||
|
||||
t.Run("reduce for building structs", func(t *testing.T) {
|
||||
type Settings struct {
|
||||
Theme string
|
||||
FontSize int
|
||||
}
|
||||
|
||||
withTheme := func(theme string) Endomorphism[Settings] {
|
||||
return func(s Settings) Settings {
|
||||
s.Theme = theme
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
withFontSize := func(size int) Endomorphism[Settings] {
|
||||
return func(s Settings) Settings {
|
||||
s.FontSize = size
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
// LEFT-TO-RIGHT application
|
||||
result := Reduce([]Endomorphism[Settings]{
|
||||
withTheme("dark"),
|
||||
withFontSize(14),
|
||||
})
|
||||
|
||||
assert.Equal(t, "dark", result.Theme)
|
||||
assert.Equal(t, 14, result.FontSize)
|
||||
})
|
||||
|
||||
t.Run("reduce is equivalent to Build(ConcatAll(reverse))", func(t *testing.T) {
|
||||
addTen := N.Add(10)
|
||||
endos := []Endomorphism[int]{addTen, double, increment}
|
||||
|
||||
// Reduce applies left-to-right
|
||||
result1 := Reduce(endos)
|
||||
|
||||
// Reverse and use ConcatAll (which is right-to-left)
|
||||
reversed := []Endomorphism[int]{increment, double, addTen}
|
||||
result2 := Build(ConcatAll(reversed))
|
||||
|
||||
assert.Equal(t, result1, result2, "Reduce should be equivalent to Build(ConcatAll(reverse))")
|
||||
})
|
||||
}
|
||||
|
||||
// TestConcatAllVsReduce demonstrates the difference between ConcatAll and Reduce
|
||||
func TestConcatAllVsReduce(t *testing.T) {
|
||||
addTen := N.Add(10)
|
||||
|
||||
endos := []Endomorphism[int]{addTen, double, increment}
|
||||
|
||||
// ConcatAll: RIGHT-TO-LEFT composition, returns endomorphism
|
||||
concatResult := ConcatAll(endos)(5)
|
||||
// 5 -> increment(5) = 6 -> double(6) = 12 -> addTen(12) = 22
|
||||
|
||||
// Reduce: LEFT-TO-RIGHT application, returns value from zero
|
||||
reduceResult := Reduce(endos)
|
||||
// 0 -> addTen(0) = 10 -> double(10) = 20 -> increment(20) = 21
|
||||
|
||||
assert.NotEqual(t, concatResult, reduceResult, "ConcatAll and Reduce should produce different results")
|
||||
assert.Equal(t, 22, concatResult, "ConcatAll should execute right-to-left on input value")
|
||||
assert.Equal(t, 21, reduceResult, "Reduce should execute left-to-right from zero value")
|
||||
}
|
||||
|
||||
// TestReduceWithBuild demonstrates using Reduce vs Build with ConcatAll
|
||||
func TestReduceWithBuild(t *testing.T) {
|
||||
addFive := N.Add(5)
|
||||
multiplyByThree := N.Mul(3)
|
||||
|
||||
endos := []Endomorphism[int]{addFive, multiplyByThree}
|
||||
|
||||
// Reduce: LEFT-TO-RIGHT from zero
|
||||
reduceResult := Reduce(endos)
|
||||
// 0 -> addFive(0) = 5 -> multiplyByThree(5) = 15
|
||||
assert.Equal(t, 15, reduceResult)
|
||||
|
||||
// Build with ConcatAll: RIGHT-TO-LEFT from zero
|
||||
buildResult := Build(ConcatAll(endos))
|
||||
// 0 -> multiplyByThree(0) = 0 -> addFive(0) = 5
|
||||
assert.Equal(t, 5, buildResult)
|
||||
|
||||
assert.NotEqual(t, reduceResult, buildResult, "Reduce and Build(ConcatAll) produce different results due to execution order")
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ func Identity[A any]() Endomorphism[A] {
|
||||
//
|
||||
// sg := endomorphism.Semigroup[int]()
|
||||
// double := N.Mul(2)
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// increment := N.Add(1)
|
||||
//
|
||||
// // Combine using the semigroup (RIGHT-TO-LEFT execution)
|
||||
// combined := sg.Concat(double, increment)
|
||||
@@ -140,7 +140,7 @@ func Semigroup[A any]() S.Semigroup[Endomorphism[A]] {
|
||||
//
|
||||
// monoid := endomorphism.Monoid[int]()
|
||||
// double := N.Mul(2)
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// increment := N.Add(1)
|
||||
// square := func(x int) int { return x * x }
|
||||
//
|
||||
// // Combine multiple endomorphisms (RIGHT-TO-LEFT execution)
|
||||
|
||||
@@ -30,7 +30,7 @@ type (
|
||||
//
|
||||
// // Simple endomorphisms on integers
|
||||
// double := N.Mul(2)
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// increment := N.Add(1)
|
||||
//
|
||||
// // Both are endomorphisms of type Endomorphism[int]
|
||||
// var f endomorphism.Endomorphism[int] = double
|
||||
|
||||
226
v2/idiomatic/REVIEW_SUMMARY.md
Normal file
226
v2/idiomatic/REVIEW_SUMMARY.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# Idiomatic Package Review Summary
|
||||
|
||||
**Date:** 2025-11-26
|
||||
**Reviewer:** Code Review Assistant
|
||||
|
||||
## Overview
|
||||
|
||||
This document summarizes the comprehensive review of the `idiomatic` package and its subpackages, including documentation fixes, additions, and test coverage analysis.
|
||||
|
||||
## Documentation Improvements
|
||||
|
||||
### 1. Main Package (`idiomatic/`)
|
||||
- ✅ **Status:** Documentation is comprehensive and well-structured
|
||||
- **File:** `doc.go` (505 lines)
|
||||
- **Quality:** Excellent - includes overview, performance comparisons, usage examples, and best practices
|
||||
|
||||
### 2. Option Package (`idiomatic/option/`)
|
||||
- ✅ **Fixed:** Added missing copyright headers to `types.go` and `function.go`
|
||||
- ✅ **Fixed:** Added comprehensive documentation for type aliases in `types.go`
|
||||
- ✅ **Fixed:** Enhanced function documentation in `function.go` with examples
|
||||
- ✅ **Fixed:** Added missing documentation for `FromZero`, `FromNonZero`, and `FromEq` functions
|
||||
- **Files Updated:**
|
||||
- `types.go` - Added copyright header and type documentation
|
||||
- `function.go` - Added copyright header and improved function docs
|
||||
- `option.go` - Enhanced documentation for utility functions
|
||||
|
||||
### 3. Result Package (`idiomatic/result/`)
|
||||
- ✅ **Fixed:** Added missing copyright header to `function.go`
|
||||
- ✅ **Fixed:** Enhanced function documentation with examples
|
||||
- **Files Updated:**
|
||||
- `function.go` - Added copyright header and improved documentation
|
||||
- `types.go` - Already had good documentation
|
||||
|
||||
### 4. IOResult Package (`idiomatic/ioresult/`)
|
||||
- ✅ **Status:** Documentation is comprehensive
|
||||
- **File:** `doc.go` (198 lines)
|
||||
- **Quality:** Excellent - includes detailed explanations of IO operations, lazy evaluation, and side effects
|
||||
|
||||
### 5. ReaderIOResult Package (`idiomatic/readerioresult/`)
|
||||
- ✅ **Created:** New `doc.go` file (96 lines)
|
||||
- ✅ **Fixed:** Added comprehensive type documentation to `types.go`
|
||||
- **New Documentation Includes:**
|
||||
- Package overview and use cases
|
||||
- Basic usage examples
|
||||
- Composition patterns
|
||||
- Error handling strategies
|
||||
- Relationship to other monads
|
||||
|
||||
### 6. ReaderResult Package (`idiomatic/readerresult/`)
|
||||
- ✅ **Fixed:** Added comprehensive type documentation to `types.go`
|
||||
- **Existing:** `doc.go` already present (178 lines) with excellent documentation
|
||||
|
||||
## Test Coverage Analysis
|
||||
|
||||
### Option Package Tests
|
||||
**File:** `idiomatic/option/option_test.go`
|
||||
|
||||
**Existing Coverage:**
|
||||
- ✅ `IsNone` - Tested
|
||||
- ✅ `IsSome` - Tested
|
||||
- ✅ `Map` - Tested
|
||||
- ✅ `Ap` - Tested
|
||||
- ✅ `Chain` - Tested
|
||||
- ✅ `ChainTo` - Comprehensive tests with multiple scenarios
|
||||
|
||||
**Missing Tests (Commented Out):**
|
||||
- ⚠️ `Flatten` - Test commented out
|
||||
- ⚠️ `Fold` - Test commented out
|
||||
- ⚠️ `FromPredicate` - Test commented out
|
||||
- ⚠️ `Alt` - Test commented out
|
||||
|
||||
**Recommendations:**
|
||||
1. Uncomment and fix the commented-out tests
|
||||
2. Add tests for:
|
||||
- `FromZero`
|
||||
- `FromNonZero`
|
||||
- `FromEq`
|
||||
- `FromNillable`
|
||||
- `MapTo`
|
||||
- `GetOrElse`
|
||||
- `ChainFirst`
|
||||
- `Reduce`
|
||||
- `Filter`
|
||||
- `Flap`
|
||||
- `ToString`
|
||||
|
||||
### Result Package Tests
|
||||
**File:** `idiomatic/result/either_test.go`
|
||||
|
||||
**Existing Coverage:**
|
||||
- ✅ `IsLeft` - Tested
|
||||
- ✅ `IsRight` - Tested
|
||||
- ✅ `Map` - Tested
|
||||
- ✅ `Ap` - Tested
|
||||
- ✅ `Alt` - Tested
|
||||
- ✅ `ChainFirst` - Tested
|
||||
- ✅ `ChainOptionK` - Tested
|
||||
- ✅ `FromOption` - Tested
|
||||
- ✅ `ToString` - Tested
|
||||
|
||||
**Missing Tests:**
|
||||
- ⚠️ `Of` - Not explicitly tested
|
||||
- ⚠️ `BiMap` - Not tested
|
||||
- ⚠️ `MapTo` - Not tested
|
||||
- ⚠️ `MapLeft` - Not tested
|
||||
- ⚠️ `Chain` - Not tested
|
||||
- ⚠️ `ChainTo` - Not tested
|
||||
- ⚠️ `ToOption` - Not tested
|
||||
- ⚠️ `FromError` - Not tested
|
||||
- ⚠️ `ToError` - Not tested
|
||||
- ⚠️ `Fold` - Not tested
|
||||
- ⚠️ `FromPredicate` - Not tested
|
||||
- ⚠️ `FromNillable` - Not tested
|
||||
- ⚠️ `GetOrElse` - Not tested
|
||||
- ⚠️ `Reduce` - Not tested
|
||||
- ⚠️ `OrElse` - Not tested
|
||||
- ⚠️ `ToType` - Not tested
|
||||
- ⚠️ `Memoize` - Not tested
|
||||
- ⚠️ `Flap` - Not tested
|
||||
|
||||
### IOResult Package Tests
|
||||
**File:** `idiomatic/ioresult/monad_test.go`
|
||||
|
||||
**Existing Coverage:** ✅ **EXCELLENT**
|
||||
- ✅ Comprehensive monad law tests (left identity, right identity, associativity)
|
||||
- ✅ Functor law tests (composition, identity)
|
||||
- ✅ Pointed, Functor, and Monad interface tests
|
||||
- ✅ Parallel vs Sequential execution tests
|
||||
- ✅ Integration tests with complex pipelines
|
||||
- ✅ Error handling scenarios
|
||||
|
||||
**Status:** This package has exemplary test coverage and can serve as a model for other packages.
|
||||
|
||||
### ReaderIOResult Package
|
||||
**Status:** ⚠️ **NO TESTS FOUND**
|
||||
|
||||
**Recommendations:**
|
||||
Create comprehensive test suite covering:
|
||||
- Basic construction and execution
|
||||
- Map, Chain, Ap operations
|
||||
- Error handling
|
||||
- Environment dependency injection
|
||||
- Integration with IOResult
|
||||
|
||||
### ReaderResult Package
|
||||
**Files:** Multiple test files exist
|
||||
- `array_test.go`
|
||||
- `bind_test.go`
|
||||
- `curry_test.go`
|
||||
- `from_test.go`
|
||||
- `monoid_test.go`
|
||||
- `reader_test.go`
|
||||
- `sequence_test.go`
|
||||
|
||||
**Status:** ✅ Good coverage exists
|
||||
|
||||
## Subpackages Review
|
||||
|
||||
### Packages Requiring Review:
|
||||
1. **idiomatic/option/number/** - Needs documentation and test review
|
||||
2. **idiomatic/option/testing/** - Contains disabled test files (`laws_test._go`, `laws._go`)
|
||||
3. **idiomatic/result/exec/** - Needs review
|
||||
4. **idiomatic/result/http/** - Needs review
|
||||
5. **idiomatic/result/testing/** - Contains disabled test files
|
||||
6. **idiomatic/ioresult/exec/** - Needs review
|
||||
7. **idiomatic/ioresult/file/** - Needs review
|
||||
8. **idiomatic/ioresult/http/** - Needs review
|
||||
9. **idiomatic/ioresult/http/builder/** - Needs review
|
||||
10. **idiomatic/ioresult/testing/** - Needs review
|
||||
|
||||
## Priority Recommendations
|
||||
|
||||
### High Priority
|
||||
1. **Enable Commented Tests:** Uncomment and fix tests in `option/option_test.go`
|
||||
2. **Add Missing Option Tests:** Create tests for all untested functions in option package
|
||||
3. **Add Missing Result Tests:** Create comprehensive test suite for result package
|
||||
4. **Create ReaderIOResult Tests:** This package has no tests at all
|
||||
|
||||
### Medium Priority
|
||||
5. **Review Subpackages:** Systematically review exec, file, http, and testing subpackages
|
||||
6. **Enable Testing Package Tests:** Investigate why `laws_test._go` files are disabled
|
||||
|
||||
### Low Priority
|
||||
7. **Benchmark Tests:** Consider adding benchmark tests for performance-critical operations
|
||||
8. **Property-Based Tests:** Consider adding property-based tests using testing/quick
|
||||
|
||||
## Files Modified in This Review
|
||||
|
||||
1. `idiomatic/option/types.go` - Added copyright and documentation
|
||||
2. `idiomatic/option/function.go` - Added copyright and enhanced docs
|
||||
3. `idiomatic/option/option.go` - Enhanced function documentation
|
||||
4. `idiomatic/result/function.go` - Added copyright and enhanced docs
|
||||
5. `idiomatic/readerioresult/doc.go` - **CREATED NEW FILE**
|
||||
6. `idiomatic/readerioresult/types.go` - Added comprehensive type docs
|
||||
7. `idiomatic/readerresult/types.go` - Added comprehensive type docs
|
||||
|
||||
## Summary Statistics
|
||||
|
||||
- **Packages Reviewed:** 6 main packages
|
||||
- **Documentation Files Created:** 1 (readerioresult/doc.go)
|
||||
- **Files Modified:** 7
|
||||
- **Lines of Documentation Added:** ~150+
|
||||
- **Test Coverage Status:**
|
||||
- ✅ Excellent: ioresult
|
||||
- ✅ Good: readerresult
|
||||
- ⚠️ Needs Improvement: option, result
|
||||
- ⚠️ Missing: readerioresult
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Create missing unit tests for option package functions
|
||||
2. Create missing unit tests for result package functions
|
||||
3. Create complete test suite for readerioresult package
|
||||
4. Review and document subpackages (exec, file, http, testing, number)
|
||||
5. Investigate and potentially enable disabled test files in testing subpackages
|
||||
6. Consider adding integration tests that demonstrate real-world usage patterns
|
||||
|
||||
## Conclusion
|
||||
|
||||
The idiomatic package has excellent documentation at the package level, with comprehensive explanations of concepts, usage patterns, and performance characteristics. The main areas for improvement are:
|
||||
|
||||
1. **Test Coverage:** Several functions lack unit tests, particularly in option and result packages
|
||||
2. **Subpackage Documentation:** Some subpackages need documentation review
|
||||
3. **Disabled Tests:** Some test files are disabled and should be investigated
|
||||
|
||||
The IOResult package serves as an excellent example of comprehensive testing, including monad law verification and integration tests. This approach should be replicated across other packages.
|
||||
@@ -27,21 +27,21 @@
|
||||
// Unlike the standard fp-go packages (option, either, result) which use struct wrappers,
|
||||
// the idiomatic package uses Go's native tuple patterns:
|
||||
//
|
||||
// Standard either: Either[E, A] (struct wrapper)
|
||||
// Idiomatic result: (A, error) (native Go tuple)
|
||||
// Standard either: Either[E, A] (struct wrapper)
|
||||
// Idiomatic result: (A, error) (native Go tuple)
|
||||
//
|
||||
// Standard option: Option[A] (struct wrapper)
|
||||
// Idiomatic option: (A, bool) (native Go tuple)
|
||||
// Standard option: Option[A] (struct wrapper)
|
||||
// Idiomatic option: (A, bool) (native Go tuple)
|
||||
//
|
||||
// # Performance Benefits
|
||||
//
|
||||
// The idiomatic approach offers several performance advantages:
|
||||
//
|
||||
// - Zero allocation for creating values (no heap allocations)
|
||||
// - Better CPU cache locality (no pointer indirection)
|
||||
// - Native Go compiler optimizations for tuples
|
||||
// - Reduced garbage collection pressure
|
||||
// - Smaller memory footprint
|
||||
// - Zero allocation for creating values (no heap allocations)
|
||||
// - Better CPU cache locality (no pointer indirection)
|
||||
// - Native Go compiler optimizations for tuples
|
||||
// - Reduced garbage collection pressure
|
||||
// - Smaller memory footprint
|
||||
//
|
||||
// Benchmarks show 2-10x performance improvements for common operations compared to struct-based
|
||||
// implementations, especially for simple operations like Map, Chain, and Fold.
|
||||
@@ -74,7 +74,7 @@
|
||||
// none := option.None[int]() // (0, false)
|
||||
//
|
||||
// // Transforming values
|
||||
// double := option.Map(func(x int) int { return x * 2 })
|
||||
// double := option.Map(N.Mul(2))
|
||||
// result := double(some) // (84, true)
|
||||
// result = double(none) // (0, false)
|
||||
//
|
||||
@@ -103,7 +103,7 @@
|
||||
// failure := result.Left[int](errors.New("oops")) // (0, error)
|
||||
//
|
||||
// // Transforming values
|
||||
// double := result.Map(func(x int) int { return x * 2 })
|
||||
// double := result.Map(N.Mul(2))
|
||||
// res := double(success) // (84, nil)
|
||||
// res = double(failure) // (0, error)
|
||||
//
|
||||
@@ -175,11 +175,11 @@
|
||||
// )()
|
||||
//
|
||||
// Key features:
|
||||
// - Lazy evaluation: Operations are not executed until the IOResult is called
|
||||
// - Composable: Chain IO operations that may fail
|
||||
// - Error handling: Automatic error propagation and recovery
|
||||
// - Resource safety: Bracket ensures proper resource cleanup
|
||||
// - Parallel execution: ApPar and TraverseArrayPar for concurrent operations
|
||||
// - Lazy evaluation: Operations are not executed until the IOResult is called
|
||||
// - Composable: Chain IO operations that may fail
|
||||
// - Error handling: Automatic error propagation and recovery
|
||||
// - Resource safety: Bracket ensures proper resource cleanup
|
||||
// - Parallel execution: ApPar and TraverseArrayPar for concurrent operations
|
||||
//
|
||||
// # Type Signatures
|
||||
//
|
||||
@@ -204,33 +204,33 @@
|
||||
// # When to Use Idiomatic vs Standard Packages
|
||||
//
|
||||
// Use idiomatic packages when:
|
||||
// - Performance is critical (hot paths, tight loops)
|
||||
// - You want zero-allocation functional patterns
|
||||
// - You prefer Go's native error handling style
|
||||
// - You're integrating with existing Go code that uses tuples
|
||||
// - Memory efficiency matters (embedded systems, high-scale services)
|
||||
// - You need IO operations with error handling (use ioresult)
|
||||
// - Performance is critical (hot paths, tight loops)
|
||||
// - You want zero-allocation functional patterns
|
||||
// - You prefer Go's native error handling style
|
||||
// - You're integrating with existing Go code that uses tuples
|
||||
// - Memory efficiency matters (embedded systems, high-scale services)
|
||||
// - You need IO operations with error handling (use ioresult)
|
||||
//
|
||||
// Use standard packages when:
|
||||
// - You need full algebraic data type semantics
|
||||
// - You're porting code from other FP languages
|
||||
// - You want explicit Either[E, A] with custom error types
|
||||
// - You need the complete suite of FP abstractions
|
||||
// - Code clarity outweighs performance concerns
|
||||
// - You need full algebraic data type semantics
|
||||
// - You're porting code from other FP languages
|
||||
// - You want explicit Either[E, A] with custom error types
|
||||
// - You need the complete suite of FP abstractions
|
||||
// - Code clarity outweighs performance concerns
|
||||
//
|
||||
// # Choosing Between result and ioresult
|
||||
//
|
||||
// Use result when:
|
||||
// - Operations are pure (same input always produces same output)
|
||||
// - No side effects are involved (no IO, no state mutation)
|
||||
// - You want to represent success/failure without execution delay
|
||||
// - Operations are pure (same input always produces same output)
|
||||
// - No side effects are involved (no IO, no state mutation)
|
||||
// - You want to represent success/failure without execution delay
|
||||
//
|
||||
// Use ioresult when:
|
||||
// - Operations perform IO (file system, network, database)
|
||||
// - Side effects are part of the computation
|
||||
// - You need lazy evaluation (defer execution until needed)
|
||||
// - You want to compose IO operations that may fail
|
||||
// - Resource management is required (files, connections, locks)
|
||||
// - Operations perform IO (file system, network, database)
|
||||
// - Side effects are part of the computation
|
||||
// - You need lazy evaluation (defer execution until needed)
|
||||
// - You want to compose IO operations that may fail
|
||||
// - Resource management is required (files, connections, locks)
|
||||
//
|
||||
// # Performance Comparison
|
||||
//
|
||||
@@ -473,7 +473,7 @@
|
||||
// result, err := F.Pipe2(
|
||||
// input,
|
||||
// result.Right[int],
|
||||
// result.Map(func(x int) int { return x * 2 }),
|
||||
// result.Map(N.Mul(2)),
|
||||
// )
|
||||
// assert.NoError(t, err)
|
||||
// assert.Equal(t, 42, result)
|
||||
@@ -483,7 +483,7 @@
|
||||
// value, ok := F.Pipe2(
|
||||
// 42,
|
||||
// option.Some[int],
|
||||
// option.Map(func(x int) int { return x * 2 }),
|
||||
// option.Map(N.Mul(2)),
|
||||
// )
|
||||
// assert.True(t, ok)
|
||||
// assert.Equal(t, 84, value)
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -147,7 +148,7 @@ func TestApFirst(t *testing.T) {
|
||||
result := F.Pipe2(
|
||||
Of(5),
|
||||
ApFirst[int](Of("ignored")),
|
||||
Map(func(x int) int { return x * 2 }),
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
|
||||
val, err := result()
|
||||
@@ -298,7 +299,7 @@ func TestApSecond(t *testing.T) {
|
||||
result := F.Pipe2(
|
||||
Of(1),
|
||||
ApSecond[int](Of(5)),
|
||||
Map(func(x int) int { return x * 2 }),
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
|
||||
val, err := result()
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
func BenchmarkOf(b *testing.B) {
|
||||
@@ -29,7 +30,7 @@ func BenchmarkOf(b *testing.B) {
|
||||
|
||||
func BenchmarkMap(b *testing.B) {
|
||||
io := Of(42)
|
||||
f := func(x int) int { return x * 2 }
|
||||
f := N.Mul(2)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -68,9 +69,9 @@ func BenchmarkBind(b *testing.B) {
|
||||
}
|
||||
|
||||
func BenchmarkPipeline(b *testing.B) {
|
||||
f1 := func(x int) int { return x + 1 }
|
||||
f2 := func(x int) int { return x * 2 }
|
||||
f3 := func(x int) int { return x - 3 }
|
||||
f1 := N.Add(1)
|
||||
f2 := N.Mul(2)
|
||||
f3 := N.Sub(3)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -93,9 +94,9 @@ func BenchmarkExecute(b *testing.B) {
|
||||
}
|
||||
|
||||
func BenchmarkExecutePipeline(b *testing.B) {
|
||||
f1 := func(x int) int { return x + 1 }
|
||||
f2 := func(x int) int { return x * 2 }
|
||||
f3 := func(x int) int { return x - 3 }
|
||||
f1 := N.Add(1)
|
||||
f2 := N.Mul(2)
|
||||
f3 := N.Sub(3)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -133,7 +134,7 @@ func BenchmarkLeft(b *testing.B) {
|
||||
|
||||
func BenchmarkMapWithError(b *testing.B) {
|
||||
io := Left[int](F.Constant[error](nil)())
|
||||
f := func(x int) int { return x * 2 }
|
||||
f := N.Mul(2)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -194,7 +195,7 @@ func BenchmarkMonadApSecond(b *testing.B) {
|
||||
func BenchmarkFunctor(b *testing.B) {
|
||||
functor := Functor[int, int]()
|
||||
io := Of(42)
|
||||
f := func(x int) int { return x * 2 }
|
||||
f := N.Mul(2)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
||||
@@ -45,23 +45,23 @@
|
||||
//
|
||||
// IOResult provides two critical benefits:
|
||||
//
|
||||
// 1. **Lazy Evaluation**: The side effect doesn't happen when you create the IOResult,
|
||||
// only when you call it (execute it). This allows you to build complex computations
|
||||
// as pure data structures and defer execution until needed.
|
||||
// 1. **Lazy Evaluation**: The side effect doesn't happen when you create the IOResult,
|
||||
// only when you call it (execute it). This allows you to build complex computations
|
||||
// as pure data structures and defer execution until needed.
|
||||
//
|
||||
// // This doesn't read the file yet, just describes how to read it
|
||||
// readConfig := func() (Config, error) { return os.ReadFile("config.json") }
|
||||
// // This doesn't read the file yet, just describes how to read it
|
||||
// readConfig := func() (Config, error) { return os.ReadFile("config.json") }
|
||||
//
|
||||
// // Still hasn't read the file, just composed operations
|
||||
// parsed := Map(parseJSON)(readConfig)
|
||||
// // Still hasn't read the file, just composed operations
|
||||
// parsed := Map(parseJSON)(readConfig)
|
||||
//
|
||||
// // NOW it reads the file and parses it
|
||||
// config, err := parsed()
|
||||
// // NOW it reads the file and parses it
|
||||
// config, err := parsed()
|
||||
//
|
||||
// 2. **Referential Transparency of the Description**: While the IO operation itself has
|
||||
// side effects, the IOResult value (the function) is referentially transparent. You can
|
||||
// pass it around, compose it, and reason about it without triggering the side effect.
|
||||
// The side effect only occurs when you explicitly call the function.
|
||||
// 2. **Referential Transparency of the Description**: While the IO operation itself has
|
||||
// side effects, the IOResult value (the function) is referentially transparent. You can
|
||||
// pass it around, compose it, and reason about it without triggering the side effect.
|
||||
// The side effect only occurs when you explicitly call the function.
|
||||
//
|
||||
// # Distinguishing Pure from Impure Operations
|
||||
//
|
||||
@@ -135,7 +135,7 @@
|
||||
//
|
||||
// Transforming values:
|
||||
//
|
||||
// doubled := Map(func(x int) int { return x * 2 })(success)
|
||||
// doubled := Map(N.Mul(2))(success)
|
||||
//
|
||||
// Chaining computations:
|
||||
//
|
||||
|
||||
@@ -33,7 +33,7 @@ func ExampleIOResult_extraction() {
|
||||
|
||||
// Or more directly using GetOrElse
|
||||
infallibleIO := GetOrElse(F.Constant1[error](io.Of(0)))(someIOResult) // => io returns 42
|
||||
valueFromIO := infallibleIO() // => 42
|
||||
valueFromIO := infallibleIO() // => 42
|
||||
|
||||
fmt.Println(value)
|
||||
fmt.Println(valueFromIO)
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -78,7 +79,7 @@ func TestFunctorMap(t *testing.T) {
|
||||
t.Run("Maps over successful value", func(t *testing.T) {
|
||||
functor := Functor[int, int]()
|
||||
io := Of(5)
|
||||
mapped := functor.Map(func(x int) int { return x * 2 })(io)
|
||||
mapped := functor.Map(N.Mul(2))(io)
|
||||
|
||||
val, err := mapped()
|
||||
assert.NoError(t, err)
|
||||
@@ -88,7 +89,7 @@ func TestFunctorMap(t *testing.T) {
|
||||
t.Run("Maps over error preserves error", func(t *testing.T) {
|
||||
functor := Functor[int, int]()
|
||||
io := Left[int](errors.New("test error"))
|
||||
mapped := functor.Map(func(x int) int { return x * 2 })(io)
|
||||
mapped := functor.Map(N.Mul(2))(io)
|
||||
|
||||
_, err := mapped()
|
||||
assert.Error(t, err)
|
||||
@@ -161,7 +162,7 @@ func TestMonadChain(t *testing.T) {
|
||||
func TestMonadAp(t *testing.T) {
|
||||
t.Run("Applies function to value", func(t *testing.T) {
|
||||
monad := Monad[int, int]()
|
||||
fn := Of(func(x int) int { return x * 2 })
|
||||
fn := Of(N.Mul(2))
|
||||
val := monad.Of(5)
|
||||
result := monad.Ap(val)(fn)
|
||||
|
||||
@@ -183,7 +184,7 @@ func TestMonadAp(t *testing.T) {
|
||||
|
||||
t.Run("Error in value", func(t *testing.T) {
|
||||
monad := Monad[int, int]()
|
||||
fn := Of(func(x int) int { return x * 2 })
|
||||
fn := Of(N.Mul(2))
|
||||
val := Left[int](errors.New("value error"))
|
||||
result := monad.Ap(val)(fn)
|
||||
|
||||
@@ -423,7 +424,7 @@ func TestFunctorComposition(t *testing.T) {
|
||||
functor2 := Functor[int, string]()
|
||||
|
||||
m := Of(5)
|
||||
f := func(x int) int { return x * 2 }
|
||||
f := N.Mul(2)
|
||||
g := func(x int) string { return fmt.Sprintf("value: %d", x) }
|
||||
|
||||
// Map(g . f)
|
||||
@@ -444,7 +445,7 @@ func TestFunctorComposition(t *testing.T) {
|
||||
functor2 := Functor[int, string]()
|
||||
|
||||
m := Left[int](errors.New("test error"))
|
||||
f := func(x int) int { return x * 2 }
|
||||
f := N.Mul(2)
|
||||
g := func(x int) string { return fmt.Sprintf("value: %d", x) }
|
||||
|
||||
composed := functor2.Map(F.Flow2(f, g))(m)
|
||||
@@ -497,7 +498,7 @@ func TestMonadParVsSeq(t *testing.T) {
|
||||
monadSeq := MonadSeq[int, int]()
|
||||
|
||||
io := Of(5)
|
||||
f := func(x int) int { return x * 2 }
|
||||
f := N.Mul(2)
|
||||
|
||||
par := monadPar.Map(f)(io)
|
||||
seq := monadSeq.Map(f)(io)
|
||||
@@ -533,7 +534,7 @@ func TestMonadParVsSeq(t *testing.T) {
|
||||
monadPar := MonadPar[int, int]()
|
||||
|
||||
io := Of(5)
|
||||
f := func(x int) int { return x * 2 }
|
||||
f := N.Mul(2)
|
||||
|
||||
def := monadDefault.Map(f)(io)
|
||||
par := monadPar.Map(f)(io)
|
||||
@@ -555,7 +556,7 @@ func TestMonadIntegration(t *testing.T) {
|
||||
// Build a pipeline: multiply by 2, add 3, then format
|
||||
result := F.Pipe2(
|
||||
monad1.Of(5),
|
||||
monad1.Map(func(x int) int { return x * 2 }),
|
||||
monad1.Map(N.Mul(2)),
|
||||
monad1.Chain(func(x int) IOResult[int] {
|
||||
return Of(x + 3)
|
||||
}),
|
||||
@@ -577,7 +578,7 @@ func TestMonadIntegration(t *testing.T) {
|
||||
|
||||
result := F.Pipe2(
|
||||
monad1.Of(5),
|
||||
monad1.Map(func(x int) int { return x * 2 }),
|
||||
monad1.Map(N.Mul(2)),
|
||||
monad1.Chain(func(x int) IOResult[int] {
|
||||
if x > 5 {
|
||||
return Left[int](errors.New("value too large"))
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
// Benchmark the closure allocations in Bind
|
||||
@@ -69,7 +70,7 @@ func BenchmarkBindAllocations(b *testing.B) {
|
||||
func BenchmarkMapPatterns(b *testing.B) {
|
||||
b.Run("SimpleFunction", func(b *testing.B) {
|
||||
io := Of(42)
|
||||
f := func(x int) int { return x * 2 }
|
||||
f := N.Mul(2)
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
@@ -85,7 +86,7 @@ func BenchmarkMapPatterns(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := Map(func(x int) int { return x * 2 })(io)
|
||||
result := Map(N.Mul(2))(io)
|
||||
_, _ = result()
|
||||
}
|
||||
})
|
||||
@@ -98,9 +99,9 @@ func BenchmarkMapPatterns(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := F.Pipe3(
|
||||
io,
|
||||
Map(func(x int) int { return x + 1 }),
|
||||
Map(func(x int) int { return x * 2 }),
|
||||
Map(func(x int) int { return x - 3 }),
|
||||
Map(N.Add(1)),
|
||||
Map(N.Mul(2)),
|
||||
Map(N.Sub(3)),
|
||||
)
|
||||
_, _ = result()
|
||||
}
|
||||
@@ -188,7 +189,7 @@ func BenchmarkErrorPaths(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := F.Pipe2(
|
||||
Of(42),
|
||||
Map(func(x int) int { return x * 2 }),
|
||||
Map(N.Mul(2)),
|
||||
Chain(func(x int) IOResult[int] { return Of(x + 1) }),
|
||||
)
|
||||
_, _ = result()
|
||||
@@ -201,7 +202,7 @@ func BenchmarkErrorPaths(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := F.Pipe2(
|
||||
Left[int](errors.New("error")),
|
||||
Map(func(x int) int { return x * 2 }),
|
||||
Map(N.Mul(2)),
|
||||
Chain(func(x int) IOResult[int] { return Of(x + 1) }),
|
||||
)
|
||||
_, _ = result()
|
||||
|
||||
@@ -17,6 +17,8 @@ package option
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
// Benchmark basic construction
|
||||
@@ -46,7 +48,7 @@ func BenchmarkIsSome(b *testing.B) {
|
||||
|
||||
func BenchmarkMap(b *testing.B) {
|
||||
v, ok := Some(21)
|
||||
mapper := Map(func(x int) int { return x * 2 })
|
||||
mapper := Map(N.Mul(2))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
||||
@@ -17,6 +17,8 @@ package option
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
// Benchmark shallow chain (1 step)
|
||||
@@ -81,11 +83,11 @@ func BenchmarkMap_5Steps(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
v1, ok1 := Map(func(x int) int { return x + 1 })(v, ok)
|
||||
v2, ok2 := Map(func(x int) int { return x * 3 })(v1, ok1)
|
||||
v3, ok3 := Map(func(x int) int { return x + 20 })(v2, ok2)
|
||||
v4, ok4 := Map(func(x int) int { return x / 2 })(v3, ok3)
|
||||
_, _ = Map(func(x int) int { return x - 10 })(v4, ok4)
|
||||
v1, ok1 := Map(N.Add(1))(v, ok)
|
||||
v2, ok2 := Map(N.Mul(3))(v1, ok1)
|
||||
v3, ok3 := Map(N.Add(20))(v2, ok2)
|
||||
v4, ok4 := Map(N.Div(2))(v3, ok3)
|
||||
_, _ = Map(N.Sub(10))(v4, ok4)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
//
|
||||
// Map transforms the contained value:
|
||||
//
|
||||
// double := Map(func(x int) int { return x * 2 })
|
||||
// double := Map(N.Mul(2))
|
||||
// result := double(Some(21)) // (42, true)
|
||||
// result := double(None[int]()) // (0, false)
|
||||
//
|
||||
@@ -113,7 +113,7 @@
|
||||
//
|
||||
// Applicative example:
|
||||
//
|
||||
// fab := Some(func(x int) int { return x * 2 })
|
||||
// fab := Some(N.Mul(2))
|
||||
// fa := Some(21)
|
||||
// result := Ap[int](fa)(fab) // (42, true)
|
||||
//
|
||||
|
||||
@@ -1,15 +1,39 @@
|
||||
// Copyright (c) 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 option
|
||||
|
||||
// Pipe1 takes an initial value t0 and successively applies 1 functions where the input of a function is the return value of the previous function
|
||||
// The final return value is the result of the last function application
|
||||
// Pipe1 takes an initial value t0 and successively applies 1 function where the input of a function is the return value of the previous function.
|
||||
// The final return value is the result of the last function application.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := Pipe1(42, func(x int) (int, bool) { return x * 2, true }) // (84, true)
|
||||
//
|
||||
//go:inline
|
||||
func Pipe1[F1 ~func(T0) (T1, bool), T0, T1 any](t0 T0, f1 F1) (T1, bool) {
|
||||
return f1(t0)
|
||||
}
|
||||
|
||||
// Flow1 creates a function that takes an initial value t0 and successively applies 1 functions where the input of a function is the return value of the previous function
|
||||
// The final return value is the result of the last function application
|
||||
// Flow1 creates a function that takes an initial value t0 and successively applies 1 function where the input of a function is the return value of the previous function.
|
||||
// The final return value is the result of the last function application.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// double := Flow1(func(x int, ok bool) (int, bool) { return x * 2, ok })
|
||||
// result := double(42, true) // (84, true)
|
||||
//
|
||||
//go:inline
|
||||
func Flow1[F1 ~func(T0, bool) (T1, bool), T0, T1 any](f1 F1) func(T0, bool) (T1, bool) {
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/eq"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -206,13 +207,13 @@ func TestFlow5(t *testing.T) {
|
||||
func TestMakeFunctor(t *testing.T) {
|
||||
t.Run("Map with functor", func(t *testing.T) {
|
||||
f := MakeFunctor[int, int]()
|
||||
double := f.Map(func(x int) int { return x * 2 })
|
||||
double := f.Map(N.Mul(2))
|
||||
AssertEq(Some(42))(double(Some(21)))(t)
|
||||
})
|
||||
|
||||
t.Run("Map with None", func(t *testing.T) {
|
||||
f := MakeFunctor[int, int]()
|
||||
double := f.Map(func(x int) int { return x * 2 })
|
||||
double := f.Map(N.Mul(2))
|
||||
AssertEq(None[int]())(double(None[int]()))(t)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
// result, ok := none // ok == false, result == 0
|
||||
//
|
||||
// // Transforming Options
|
||||
// doubled := Map(func(x int) int { return x * 2 })(some) // (84, true)
|
||||
// doubled := Map(N.Mul(2))(some) // (84, true)
|
||||
package option
|
||||
|
||||
import (
|
||||
@@ -56,16 +56,47 @@ func FromPredicate[A any](pred func(A) bool) Kleisli[A, A] {
|
||||
}
|
||||
}
|
||||
|
||||
// FromZero returns a function that creates an Option based on whether a value is the zero value.
|
||||
// Returns Some if the value is the zero value, None otherwise.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// checkZero := FromZero[int]()
|
||||
// result := checkZero(0) // Some(0)
|
||||
// result := checkZero(5) // None
|
||||
//
|
||||
//go:inline
|
||||
func FromZero[A comparable]() Kleisli[A, A] {
|
||||
return FromPredicate(P.IsZero[A]())
|
||||
}
|
||||
|
||||
// FromNonZero returns a function that creates an Option based on whether a value is non-zero.
|
||||
// Returns Some if the value is non-zero, None otherwise.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// checkNonZero := FromNonZero[int]()
|
||||
// result := checkNonZero(5) // Some(5)
|
||||
// result := checkNonZero(0) // None
|
||||
//
|
||||
//go:inline
|
||||
func FromNonZero[A comparable]() Kleisli[A, A] {
|
||||
return FromPredicate(P.IsNonZero[A]())
|
||||
}
|
||||
|
||||
// FromEq returns a function that creates an Option based on equality with a given value.
|
||||
// The returned function takes a value to compare against and returns a Kleisli function.
|
||||
//
|
||||
// Parameters:
|
||||
// - pred: An equality predicate
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import "github.com/IBM/fp-go/v2/eq"
|
||||
// equals42 := FromEq(eq.FromStrictEquals[int]())(42)
|
||||
// result := equals42(42) // Some(42)
|
||||
// result := equals42(10) // None
|
||||
//
|
||||
//go:inline
|
||||
func FromEq[A any](pred eq.Eq[A]) func(A) Kleisli[A, A] {
|
||||
return F.Flow2(P.IsEqual(pred), FromPredicate[A])
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
// Copyright (c) 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 option
|
||||
|
||||
import (
|
||||
@@ -7,6 +22,10 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
Seq[T any] = iter.Seq[T]
|
||||
// Seq is an iterator sequence type alias for working with Go 1.23+ iterators.
|
||||
Seq[T any] = iter.Seq[T]
|
||||
|
||||
// Endomorphism represents a function from type T to type T.
|
||||
// It is commonly used for transformations that preserve the type.
|
||||
Endomorphism[T any] = endomorphism.Endomorphism[T]
|
||||
)
|
||||
|
||||
112
v2/idiomatic/readerioresult/doc.go
Normal file
112
v2/idiomatic/readerioresult/doc.go
Normal file
@@ -0,0 +1,112 @@
|
||||
// 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 readerioresult provides a ReaderIOResult monad that combines Reader, IO, and Result monads.
|
||||
//
|
||||
// A ReaderIOResult[R, A] represents a computation that:
|
||||
// - Depends on an environment of type R (Reader aspect)
|
||||
// - Performs IO operations (IO aspect)
|
||||
// - May fail with an error (Result aspect, which is Either[error, A])
|
||||
//
|
||||
// This is equivalent to Reader[R, IOResult[A]] or Reader[R, func() (A, error)].
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// ReaderIOResult is particularly useful for:
|
||||
//
|
||||
// 1. Dependency injection with IO and error handling - pass configuration/services through
|
||||
// computations that perform side effects and may fail
|
||||
// 2. Functional IO with context - compose IO operations that depend on environment and may error
|
||||
// 3. Testing - easily mock dependencies and IO operations by changing the environment value
|
||||
// 4. Resource management - manage resources that depend on configuration
|
||||
//
|
||||
// # Basic Example
|
||||
//
|
||||
// type Config struct {
|
||||
// DatabaseURL string
|
||||
// Timeout time.Duration
|
||||
// }
|
||||
//
|
||||
// // Function that needs config, performs IO, and may fail
|
||||
// func fetchUser(id int) readerioresult.ReaderIOResult[Config, User] {
|
||||
// return func(cfg Config) ioresult.IOResult[User] {
|
||||
// return func() (User, error) {
|
||||
// // Use cfg.DatabaseURL and cfg.Timeout to fetch user
|
||||
// return queryDatabase(cfg.DatabaseURL, id, cfg.Timeout)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Execute by providing the config
|
||||
// cfg := Config{DatabaseURL: "postgres://...", Timeout: 5 * time.Second}
|
||||
// ioResult := fetchUser(42)(cfg) // Returns IOResult[User]
|
||||
// user, err := ioResult() // Execute the IO operation
|
||||
//
|
||||
// # Composition
|
||||
//
|
||||
// ReaderIOResult provides several ways to compose computations:
|
||||
//
|
||||
// 1. Map - transform successful values
|
||||
// 2. Chain (FlatMap) - sequence dependent IO operations
|
||||
// 3. Ap - combine independent IO computations
|
||||
// 4. ChainFirst - perform IO for side effects while keeping original value
|
||||
//
|
||||
// # Example with Composition
|
||||
//
|
||||
// type AppContext struct {
|
||||
// DB *sql.DB
|
||||
// Cache Cache
|
||||
// Log Logger
|
||||
// }
|
||||
//
|
||||
// getUserWithCache := F.Pipe2(
|
||||
// getFromCache(userID),
|
||||
// readerioresult.Alt(func() readerioresult.ReaderIOResult[AppContext, User] {
|
||||
// return F.Pipe2(
|
||||
// getFromDB(userID),
|
||||
// readerioresult.ChainFirst(saveToCache),
|
||||
// )
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
// ctx := AppContext{DB: db, Cache: cache, Log: logger}
|
||||
// user, err := getUserWithCache(ctx)()
|
||||
//
|
||||
// # Error Handling
|
||||
//
|
||||
// ReaderIOResult provides several functions for error handling:
|
||||
//
|
||||
// - Left/Right - create failed/successful values
|
||||
// - GetOrElse - provide a default value for errors
|
||||
// - OrElse - recover from errors with an alternative computation
|
||||
// - Fold - handle both success and failure cases
|
||||
// - ChainLeft - transform error values into new computations
|
||||
//
|
||||
// # Relationship to Other Monads
|
||||
//
|
||||
// ReaderIOResult is related to several other monads in this library:
|
||||
//
|
||||
// - Reader[R, A] - ReaderIOResult without IO or error handling
|
||||
// - IOResult[A] - ReaderIOResult without environment dependency
|
||||
// - ReaderResult[R, A] - ReaderIOResult without IO (pure computations)
|
||||
// - ReaderIO[R, A] - ReaderIOResult without error handling
|
||||
// - ReaderIOEither[R, E, A] - like ReaderIOResult but with custom error type E
|
||||
//
|
||||
// # Performance Note
|
||||
//
|
||||
// ReaderIOResult is a zero-cost abstraction - it compiles to a simple function type
|
||||
// with no runtime overhead beyond the underlying computation. The IO operations are
|
||||
// lazy and only executed when the final IOResult is called.
|
||||
package readerioresult
|
||||
66
v2/idiomatic/readerioresult/types.go
Normal file
66
v2/idiomatic/readerioresult/types.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// 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 readerioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
type (
|
||||
// Endomorphism represents a function from type A to type A.
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
// Lazy represents a deferred computation that produces a value of type A when evaluated.
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
|
||||
// Option represents an optional value that may or may not be present.
|
||||
Option[A any] = option.Option[A]
|
||||
|
||||
// Result represents an Either with error as the left type, compatible with Go's (value, error) tuple.
|
||||
Result[A any] = result.Result[A]
|
||||
|
||||
// Reader represents a computation that depends on a read-only environment of type R and produces a value of type A.
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
|
||||
// IO represents a computation that performs side effects and returns a value of type A.
|
||||
IO[A any] = io.IO[A]
|
||||
|
||||
// IOResult represents a computation that performs IO and may fail with an error.
|
||||
IOResult[A any] = ioresult.IOResult[A]
|
||||
|
||||
// ReaderIOResult represents a computation that depends on an environment R,
|
||||
// performs IO operations, and may fail with an error.
|
||||
// It is equivalent to Reader[R, IOResult[A]] or func(R) func() (A, error).
|
||||
ReaderIOResult[R, A any] = Reader[R, IOResult[A]]
|
||||
|
||||
// Monoid represents a monoid structure for ReaderIOResult values.
|
||||
Monoid[R, A any] = monoid.Monoid[ReaderIOResult[R, A]]
|
||||
|
||||
// Kleisli represents a function from A to a ReaderIOResult of B.
|
||||
// It is used for chaining computations that depend on environment, perform IO, and may fail.
|
||||
Kleisli[R, A, B any] = Reader[A, ReaderIOResult[R, B]]
|
||||
|
||||
// Operator represents a transformation from ReaderIOResult[R, A] to ReaderIOResult[R, B].
|
||||
// It is commonly used in function composition pipelines.
|
||||
Operator[R, A, B any] = Kleisli[R, ReaderIOResult[R, A], B]
|
||||
)
|
||||
284
v2/idiomatic/readerresult/benchmark_test.go
Normal file
284
v2/idiomatic/readerresult/benchmark_test.go
Normal file
@@ -0,0 +1,284 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
type BenchContext struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
// Benchmark basic operations
|
||||
|
||||
func BenchmarkOf(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
rr := Of[BenchContext](i)
|
||||
_, _ = rr(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLeft(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
err := testError
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
rr := Left[BenchContext, int](err)
|
||||
_, _ = rr(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMap(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr := Of[BenchContext](10)
|
||||
double := N.Mul(2)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
mapped := F.Pipe1(rr, Map[BenchContext](double))
|
||||
_, _ = mapped(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMapChain(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr := Of[BenchContext](1)
|
||||
double := N.Mul(2)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := F.Pipe3(
|
||||
rr,
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChain(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr := Of[BenchContext](10)
|
||||
addOne := func(x int) ReaderResult[BenchContext, int] {
|
||||
return Of[BenchContext](x + 1)
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
chained := F.Pipe1(rr, Chain(addOne))
|
||||
_, _ = chained(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChainDeep(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr := Of[BenchContext](1)
|
||||
addOne := func(x int) ReaderResult[BenchContext, int] {
|
||||
return Of[BenchContext](x + 1)
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := F.Pipe5(
|
||||
rr,
|
||||
Chain(addOne),
|
||||
Chain(addOne),
|
||||
Chain(addOne),
|
||||
Chain(addOne),
|
||||
Chain(addOne),
|
||||
)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAp(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
fab := Of[BenchContext](N.Mul(2))
|
||||
fa := Of[BenchContext](21)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := MonadAp(fab, fa)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSequenceT2(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr1 := Of[BenchContext](10)
|
||||
rr2 := Of[BenchContext](20)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := SequenceT2(rr1, rr2)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSequenceT4(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr1 := Of[BenchContext](10)
|
||||
rr2 := Of[BenchContext](20)
|
||||
rr3 := Of[BenchContext](30)
|
||||
rr4 := Of[BenchContext](40)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := SequenceT4(rr1, rr2, rr3, rr4)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDoNotation(b *testing.B) {
|
||||
ctx := context.Background()
|
||||
|
||||
type State struct {
|
||||
A int
|
||||
B int
|
||||
C int
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := F.Pipe3(
|
||||
Do[context.Context](State{}),
|
||||
Bind(
|
||||
func(a int) func(State) State {
|
||||
return func(s State) State { s.A = a; return s }
|
||||
},
|
||||
func(s State) ReaderResult[context.Context, int] {
|
||||
return Of[context.Context](10)
|
||||
},
|
||||
),
|
||||
Bind(
|
||||
func(b int) func(State) State {
|
||||
return func(s State) State { s.B = b; return s }
|
||||
},
|
||||
func(s State) ReaderResult[context.Context, int] {
|
||||
return Of[context.Context](s.A * 2)
|
||||
},
|
||||
),
|
||||
Bind(
|
||||
func(c int) func(State) State {
|
||||
return func(s State) State { s.C = c; return s }
|
||||
},
|
||||
func(s State) ReaderResult[context.Context, int] {
|
||||
return Of[context.Context](s.A + s.B)
|
||||
},
|
||||
),
|
||||
)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkErrorPropagation(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
err := testError
|
||||
rr := Left[BenchContext, int](err)
|
||||
double := N.Mul(2)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := F.Pipe5(
|
||||
rr,
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTraverseArray(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
arr := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
kleisli := func(x int) ReaderResult[BenchContext, int] {
|
||||
return Of[BenchContext](x * 2)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
traversed := TraverseArray[BenchContext](kleisli)
|
||||
result := traversed(arr)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSequenceArray(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
arr := []ReaderResult[BenchContext, int]{
|
||||
Of[BenchContext](1),
|
||||
Of[BenchContext](2),
|
||||
Of[BenchContext](3),
|
||||
Of[BenchContext](4),
|
||||
Of[BenchContext](5),
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := SequenceArray(arr)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Real-world scenario benchmarks
|
||||
|
||||
func BenchmarkRealWorldPipeline(b *testing.B) {
|
||||
type Config struct {
|
||||
Multiplier int
|
||||
Offset int
|
||||
}
|
||||
|
||||
ctx := Config{Multiplier: 5, Offset: 10}
|
||||
|
||||
type State struct {
|
||||
Input int
|
||||
Result int
|
||||
}
|
||||
|
||||
getMultiplier := func(cfg Config) int { return cfg.Multiplier }
|
||||
getOffset := func(cfg Config) int { return cfg.Offset }
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
step1 := Bind(
|
||||
func(m int) func(State) State {
|
||||
return func(s State) State { s.Result = s.Input * m; return s }
|
||||
},
|
||||
func(s State) ReaderResult[Config, int] {
|
||||
return Asks(getMultiplier)
|
||||
},
|
||||
)
|
||||
step2 := Bind(
|
||||
func(off int) func(State) State {
|
||||
return func(s State) State { s.Result += off; return s }
|
||||
},
|
||||
func(s State) ReaderResult[Config, int] {
|
||||
return Asks(getOffset)
|
||||
},
|
||||
)
|
||||
result := F.Pipe3(
|
||||
Do[Config](State{Input: 10}),
|
||||
step1,
|
||||
step2,
|
||||
Map[Config](func(s State) int { return s.Result }),
|
||||
)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ package readerresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/idiomatic/result"
|
||||
AP "github.com/IBM/fp-go/v2/internal/apply"
|
||||
C "github.com/IBM/fp-go/v2/internal/chain"
|
||||
@@ -69,7 +68,7 @@ func Do[R, S any](
|
||||
// ConfigService ConfigService
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// result := function.Pipe2(
|
||||
// readereither.Do[Env, error](State{}),
|
||||
// readereither.Bind(
|
||||
// func(user User) func(State) State {
|
||||
@@ -173,7 +172,7 @@ func BindTo[R, S1, T any](
|
||||
// return env.ConfigService.GetConfig()
|
||||
// })
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// result := function.Pipe2(
|
||||
// readereither.Do[Env, error](State{}),
|
||||
// readereither.ApS(
|
||||
// func(user User) func(State) State {
|
||||
@@ -228,7 +227,7 @@ func ApS[R, S1, S2, T any](
|
||||
// getConfig := readereither.Asks(func(env Env) either.Either[error, Config] {
|
||||
// return env.ConfigService.GetConfig()
|
||||
// })
|
||||
// result := F.Pipe2(
|
||||
// result := function.Pipe2(
|
||||
// readereither.Of[Env, error](State{}),
|
||||
// readereither.ApSL(configLens, getConfig),
|
||||
// )
|
||||
@@ -265,7 +264,7 @@ func ApSL[R, S, T any](
|
||||
// func(s State, u User) State { s.User = u; return s },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// result := function.Pipe2(
|
||||
// readereither.Do[Env, error](State{}),
|
||||
// readereither.BindL(userLens, func(user User) readereither.ReaderResult[Env, error, User] {
|
||||
// return readereither.Asks(func(env Env) either.Either[error, User] {
|
||||
@@ -279,7 +278,7 @@ func BindL[R, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Kleisli[R, T, T],
|
||||
) Operator[R, S, S] {
|
||||
return Bind(lens.Set, F.Flow2(lens.Get, f))
|
||||
return Bind(lens.Set, function.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetL is a variant of Let that uses a lens to focus on a specific part of the context.
|
||||
@@ -302,7 +301,7 @@ func BindL[R, S, T any](
|
||||
// func(s State, c Config) State { s.Config = c; return s },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// result := function.Pipe2(
|
||||
// readereither.Do[any, error](State{Config: Config{Host: "localhost"}}),
|
||||
// readereither.LetL(configLens, func(cfg Config) Config {
|
||||
// cfg.Port = 8080
|
||||
@@ -315,7 +314,7 @@ func LetL[R, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Endomorphism[T],
|
||||
) Operator[R, S, S] {
|
||||
return Let[R](lens.Set, F.Flow2(lens.Get, f))
|
||||
return Let[R](lens.Set, function.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetToL is a variant of LetTo that uses a lens to focus on a specific part of the context.
|
||||
@@ -338,7 +337,7 @@ func LetL[R, S, T any](
|
||||
// )
|
||||
//
|
||||
// newConfig := Config{Host: "localhost", Port: 8080}
|
||||
// result := F.Pipe2(
|
||||
// result := function.Pipe2(
|
||||
// readereither.Do[any, error](State{}),
|
||||
// readereither.LetToL(configLens, newConfig),
|
||||
// )
|
||||
@@ -374,7 +373,7 @@ func LetToL[R, S, T any](
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// result := function.Pipe2(
|
||||
// readerresult.Do[Env](State{}),
|
||||
// readerresult.BindReaderK(
|
||||
// func(path string) func(State) State {
|
||||
@@ -435,7 +434,7 @@ func BindEitherK[R, S1, S2, T any](
|
||||
// return result.Of(s.Value * 2)
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// result := function.Pipe2(
|
||||
// readerresult.Do[any](State{Value: 5}),
|
||||
// readerresult.BindResultK(
|
||||
// func(parsed int) func(State) State {
|
||||
@@ -481,7 +480,7 @@ func BindResultK[R, S1, S2, T any](
|
||||
// return env.ConfigPath
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// result := function.Pipe1(
|
||||
// reader.Of[Env](getConfigPath),
|
||||
// readerresult.BindToReader(func(path string) State {
|
||||
// return State{Config: path}
|
||||
@@ -530,7 +529,7 @@ func BindToEither[
|
||||
// return 42
|
||||
// })
|
||||
//
|
||||
// computation := F.Pipe1(
|
||||
// computation := function.Pipe1(
|
||||
// parseResult,
|
||||
// readerresult.BindToResult[any](func(value int) State {
|
||||
// return State{Value: value}
|
||||
@@ -572,7 +571,7 @@ func BindToResult[
|
||||
// getDefaultPort := func(env Env) int { return env.DefaultPort }
|
||||
// getDefaultHost := func(env Env) string { return env.DefaultHost }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// result := function.Pipe2(
|
||||
// readerresult.Do[Env](State{}),
|
||||
// readerresult.ApReaderS(
|
||||
// func(port int) func(State) State {
|
||||
@@ -635,7 +634,7 @@ func ApResultS[
|
||||
// parseValue1 := result.TryCatch(func() int { return 42 })
|
||||
// parseValue2 := result.TryCatch(func() int { return 100 })
|
||||
//
|
||||
// computation := F.Pipe2(
|
||||
// computation := function.Pipe2(
|
||||
// readerresult.Do[any](State{}),
|
||||
// readerresult.ApResultS(
|
||||
// func(v int) func(State) State {
|
||||
|
||||
@@ -84,6 +84,74 @@
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
// # Object-Oriented Patterns with Curry Functions
|
||||
//
|
||||
// The Curry functions enable an interesting pattern where you can treat the Reader context (R)
|
||||
// as an object instance, effectively creating method-like functions that compose functionally.
|
||||
//
|
||||
// When you curry a function like func(R, T1, T2) (A, error), the context R becomes the last
|
||||
// argument to be applied, even though it appears first in the original function signature.
|
||||
// This is intentional and follows Go's context-first convention while enabling functional
|
||||
// composition patterns.
|
||||
//
|
||||
// Why R is the last curried argument:
|
||||
//
|
||||
// - In Go, context conventionally comes first: func(ctx Context, params...) (Result, error)
|
||||
// - In curried form: Curry2(f)(param1)(param2) returns ReaderResult[R, A]
|
||||
// - The ReaderResult is then applied to R: Curry2(f)(param1)(param2)(ctx)
|
||||
// - This allows partial application of business parameters before providing the context/object
|
||||
//
|
||||
// Object-Oriented Example:
|
||||
//
|
||||
// // A service struct that acts as the Reader context
|
||||
// type UserService struct {
|
||||
// db *sql.DB
|
||||
// cache Cache
|
||||
// }
|
||||
//
|
||||
// // A method-like function following Go conventions (context first)
|
||||
// func (s *UserService) GetUserByID(ctx context.Context, id int) (User, error) {
|
||||
// // Use s.db and s.cache...
|
||||
// }
|
||||
//
|
||||
// func (s *UserService) UpdateUser(ctx context.Context, id int, name string) (User, error) {
|
||||
// // Use s.db and s.cache...
|
||||
// }
|
||||
//
|
||||
// // Curry these into composable operations
|
||||
// getUser := readerresult.Curry1((*UserService).GetUserByID)
|
||||
// updateUser := readerresult.Curry2((*UserService).UpdateUser)
|
||||
//
|
||||
// // Now compose operations that will be bound to a UserService instance
|
||||
// type Context struct {
|
||||
// Svc *UserService
|
||||
// }
|
||||
//
|
||||
// pipeline := F.Pipe2(
|
||||
// getUser(42), // ReaderResult[Context, User]
|
||||
// readerresult.Chain(func(user User) readerresult.ReaderResult[Context, User] {
|
||||
// newName := user.Name + " (updated)"
|
||||
// return updateUser(user.ID)(newName)
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
// // Execute by providing the service instance as context
|
||||
// svc := &UserService{db: db, cache: cache}
|
||||
// ctx := Context{Svc: svc}
|
||||
// updatedUser, err := pipeline(ctx)
|
||||
//
|
||||
// The key insight is that currying creates a chain where:
|
||||
// 1. Business parameters are applied first: getUser(42)
|
||||
// 2. This returns a ReaderResult that waits for the context
|
||||
// 3. Multiple operations can be composed before providing the context
|
||||
// 4. Finally, the context/object is provided to execute everything: pipeline(ctx)
|
||||
//
|
||||
// This pattern is particularly useful for:
|
||||
// - Creating reusable operation pipelines independent of service instances
|
||||
// - Testing with mock service instances
|
||||
// - Dependency injection in a functional style
|
||||
// - Composing operations that share the same service context
|
||||
//
|
||||
// # Error Handling
|
||||
//
|
||||
// ReaderResult provides several functions for error handling:
|
||||
|
||||
@@ -116,7 +116,7 @@ func FromReader[R, A any](r Reader[R, A]) ReaderResult[R, A] {
|
||||
// Example:
|
||||
//
|
||||
// rr := readerresult.Of[Config](5)
|
||||
// doubled := readerresult.MonadMap(rr, func(x int) int { return x * 2 })
|
||||
// doubled := readerresult.MonadMap(rr, N.Mul(2))
|
||||
// // doubled(cfg) returns (10, nil)
|
||||
func MonadMap[R, A, B any](fa ReaderResult[R, A], f func(A) B) ReaderResult[R, B] {
|
||||
mp := result.Map(f)
|
||||
@@ -130,7 +130,7 @@ func MonadMap[R, A, B any](fa ReaderResult[R, A], f func(A) B) ReaderResult[R, B
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// double := readerresult.Map[Config](func(x int) int { return x * 2 })
|
||||
// double := readerresult.Map[Config](N.Mul(2))
|
||||
// result := F.Pipe1(readerresult.Of[Config](5), double)
|
||||
func Map[R, A, B any](f func(A) B) Operator[R, A, B] {
|
||||
mp := result.Map(f)
|
||||
@@ -365,13 +365,13 @@ func Asks[R, A any](r Reader[R, A]) ReaderResult[R, A] {
|
||||
//
|
||||
// parseUser := func(data string) result.Result[User] { ... }
|
||||
// result := readerresult.MonadChainEitherK(getUserDataRR, parseUser)
|
||||
func MonadChainEitherK[R, A, B any](ma ReaderResult[R, A], f result.Kleisli[A, B]) ReaderResult[R, B] {
|
||||
func MonadChainEitherK[R, A, B any](ma ReaderResult[R, A], f RES.Kleisli[A, B]) ReaderResult[R, B] {
|
||||
return func(r R) (B, error) {
|
||||
a, err := ma(r)
|
||||
if err != nil {
|
||||
return result.Left[B](err)
|
||||
}
|
||||
return f(a)
|
||||
return RES.Unwrap(f(a))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,10 +384,40 @@ func MonadChainEitherK[R, A, B any](ma ReaderResult[R, A], f result.Kleisli[A, B
|
||||
// result := F.Pipe1(getUserDataRR, readerresult.ChainEitherK[Config](parseUser))
|
||||
//
|
||||
//go:inline
|
||||
func ChainEitherK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, B] {
|
||||
func ChainEitherK[R, A, B any](f RES.Kleisli[A, B]) Operator[R, A, B] {
|
||||
return function.Bind2nd(MonadChainEitherK[R, A, B], f)
|
||||
}
|
||||
|
||||
// MonadChainReaderK chains a ReaderResult with a function that returns a plain Result.
|
||||
// This is useful for integrating functions that don't need environment access.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// parseUser := func(data string) result.Result[User] { ... }
|
||||
// result := readerresult.MonadChainReaderK(getUserDataRR, parseUser)
|
||||
func MonadChainReaderK[R, A, B any](ma ReaderResult[R, A], f result.Kleisli[A, B]) ReaderResult[R, B] {
|
||||
return func(r R) (B, error) {
|
||||
a, err := ma(r)
|
||||
if err != nil {
|
||||
return result.Left[B](err)
|
||||
}
|
||||
return f(a)
|
||||
}
|
||||
}
|
||||
|
||||
// ChainReaderK is the curried version of MonadChainEitherK.
|
||||
// It lifts a Result-returning function into a ReaderResult operator.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// parseUser := func(data string) result.Result[User] { ... }
|
||||
// result := F.Pipe1(getUserDataRR, readerresult.ChainReaderK[Config](parseUser))
|
||||
//
|
||||
//go:inline
|
||||
func ChainReaderK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, B] {
|
||||
return function.Bind2nd(MonadChainReaderK[R, A, B], f)
|
||||
}
|
||||
|
||||
// ChainOptionK chains with a function that returns an Option, converting None to an error.
|
||||
// This is useful for integrating functions that return optional values.
|
||||
//
|
||||
@@ -432,7 +462,7 @@ func Flatten[R, A any](mma ReaderResult[R, ReaderResult[R, A]]) ReaderResult[R,
|
||||
// Example:
|
||||
//
|
||||
// enrichErr := func(e error) error { return fmt.Errorf("enriched: %w", e) }
|
||||
// double := func(x int) int { return x * 2 }
|
||||
// double := N.Mul(2)
|
||||
// result := readerresult.MonadBiMap(rr, enrichErr, double)
|
||||
//
|
||||
//go:inline
|
||||
@@ -452,7 +482,7 @@ func MonadBiMap[R, A, B any](fa ReaderResult[R, A], f Endomorphism[error], g fun
|
||||
// Example:
|
||||
//
|
||||
// enrichErr := func(e error) error { return fmt.Errorf("enriched: %w", e) }
|
||||
// double := func(x int) int { return x * 2 }
|
||||
// double := N.Mul(2)
|
||||
// result := F.Pipe1(rr, readerresult.BiMap[Config](enrichErr, double))
|
||||
func BiMap[R, A, B any](f Endomorphism[error], g func(A) B) Operator[R, A, B] {
|
||||
return func(fa ReaderResult[R, A]) ReaderResult[R, B] {
|
||||
@@ -500,7 +530,7 @@ func Read[A, R any](r R) func(ReaderResult[R, A]) (A, error) {
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// fabr := readerresult.Of[Config](func(x int) int { return x * 2 })
|
||||
// fabr := readerresult.Of[Config](N.Mul(2))
|
||||
// result := readerresult.MonadFlap(fabr, 5) // Returns (10, nil)
|
||||
//
|
||||
//go:inline
|
||||
|
||||
@@ -22,8 +22,10 @@ import (
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
RES "github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -116,7 +118,7 @@ func TestMap(t *testing.T) {
|
||||
|
||||
func TestMonadMap(t *testing.T) {
|
||||
rr := Of[MyContext](5)
|
||||
doubled := MonadMap(rr, func(x int) int { return x * 2 })
|
||||
doubled := MonadMap(rr, N.Mul(2))
|
||||
v, err := doubled(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 10, v)
|
||||
@@ -271,7 +273,7 @@ func TestAsks(t *testing.T) {
|
||||
assert.Equal(t, 7, v)
|
||||
}
|
||||
|
||||
func TestChainEitherK(t *testing.T) {
|
||||
func TestChainReaderK(t *testing.T) {
|
||||
parseInt := func(s string) (int, error) {
|
||||
if s == "42" {
|
||||
return 42, nil
|
||||
@@ -279,6 +281,24 @@ func TestChainEitherK(t *testing.T) {
|
||||
return 0, errors.New("parse error")
|
||||
}
|
||||
|
||||
chain := ChainReaderK[MyContext](parseInt)
|
||||
|
||||
v, err := F.Pipe1(Of[MyContext]("42"), chain)(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, v)
|
||||
|
||||
_, err = F.Pipe1(Of[MyContext]("invalid"), chain)(defaultContext)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestChainEitherK(t *testing.T) {
|
||||
parseInt := func(s string) RES.Result[int] {
|
||||
if s == "42" {
|
||||
return RES.Of(42)
|
||||
}
|
||||
return RES.Left[int](errors.New("parse error"))
|
||||
}
|
||||
|
||||
chain := ChainEitherK[MyContext](parseInt)
|
||||
|
||||
v, err := F.Pipe1(Of[MyContext]("42"), chain)(defaultContext)
|
||||
@@ -322,7 +342,7 @@ func TestFlatten(t *testing.T) {
|
||||
|
||||
func TestBiMap(t *testing.T) {
|
||||
enrichErr := func(e error) error { return fmt.Errorf("enriched: %w", e) }
|
||||
double := func(x int) int { return x * 2 }
|
||||
double := N.Mul(2)
|
||||
|
||||
res := F.Pipe1(Of[MyContext](5), BiMap[MyContext](enrichErr, double))
|
||||
v, err := res(defaultContext)
|
||||
@@ -357,7 +377,7 @@ func TestRead(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFlap(t *testing.T) {
|
||||
fabr := Of[MyContext](func(x int) int { return x * 2 })
|
||||
fabr := Of[MyContext](N.Mul(2))
|
||||
flapped := MonadFlap(fabr, 5)
|
||||
v, err := flapped(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -26,16 +26,37 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
// Endomorphism represents a function from type A to type A.
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
Option[A any] = option.Option[A]
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
Result[A any] = result.Result[A]
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
|
||||
// Lazy represents a deferred computation that produces a value of type A when evaluated.
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
|
||||
// Option represents an optional value that may or may not be present.
|
||||
Option[A any] = option.Option[A]
|
||||
|
||||
// Either represents a value that can be one of two types: Left (E) or Right (A).
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
|
||||
// Result represents an Either with error as the left type, compatible with Go's (value, error) tuple.
|
||||
Result[A any] = result.Result[A]
|
||||
|
||||
// Reader represents a computation that depends on a read-only environment of type R and produces a value of type A.
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
|
||||
// ReaderResult represents a computation that depends on an environment R and may fail with an error.
|
||||
// It is equivalent to Reader[R, Result[A]] or func(R) (A, error).
|
||||
// This combines dependency injection with error handling in a functional style.
|
||||
ReaderResult[R, A any] = func(R) (A, error)
|
||||
Monoid[R, A any] = monoid.Monoid[ReaderResult[R, A]]
|
||||
|
||||
Kleisli[R, A, B any] = Reader[A, ReaderResult[R, B]]
|
||||
// Monoid represents a monoid structure for ReaderResult values.
|
||||
Monoid[R, A any] = monoid.Monoid[ReaderResult[R, A]]
|
||||
|
||||
// Kleisli represents a function from A to a ReaderResult of B.
|
||||
// It is used for chaining computations that depend on environment and may fail.
|
||||
Kleisli[R, A, B any] = Reader[A, ReaderResult[R, B]]
|
||||
|
||||
// Operator represents a transformation from ReaderResult[R, A] to ReaderResult[R, B].
|
||||
// It is commonly used in function composition pipelines.
|
||||
Operator[R, A, B any] = Kleisli[R, ReaderResult[R, A], B]
|
||||
)
|
||||
|
||||
@@ -27,7 +27,7 @@ func Example_extraction() {
|
||||
rightValue, rightErr := Right(10)
|
||||
|
||||
// Convert Either[A] to A with a default value
|
||||
leftWithDefault := GetOrElse(F.Constant1[error](0))(leftValue, leftErr) // 0
|
||||
leftWithDefault := GetOrElse(F.Constant1[error](0))(leftValue, leftErr) // 0
|
||||
rightWithDefault := GetOrElse(F.Constant1[error](0))(rightValue, rightErr) // 10
|
||||
|
||||
// Apply a different function on Left(...)/Right(...)
|
||||
|
||||
@@ -1,7 +1,26 @@
|
||||
// Copyright (c) 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 result
|
||||
|
||||
// Pipe1 takes an initial value t0 and successively applies 1 functions where the input of a function is the return value of the previous function
|
||||
// The final return value is the result of the last function application
|
||||
// Pipe1 takes an initial value t0 and successively applies 1 function where the input of a function is the return value of the previous function.
|
||||
// The final return value is the result of the last function application.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result, err := Pipe1(42, func(x int) (int, error) { return x * 2, nil }) // (84, nil)
|
||||
//
|
||||
//go:inline
|
||||
func Pipe1[F1 ~func(T0) (T1, error), T0, T1 any](t0 T0, f1 F1) (T1, error) {
|
||||
|
||||
@@ -67,7 +67,7 @@ import (
|
||||
// })
|
||||
//
|
||||
// // ApV with both function and value having errors
|
||||
// double := func(x int) int { return x * 2 }
|
||||
// double := N.Mul(2)
|
||||
// apv := result.ApV[int, int](errorSemigroup)
|
||||
//
|
||||
// value := result.Left[int](errors.New("invalid value"))
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -67,7 +68,7 @@ func TestApV_BothRight(t *testing.T) {
|
||||
sg := makeErrorConcatSemigroup()
|
||||
apv := ApV[int, int](sg)
|
||||
|
||||
double := func(x int) int { return x * 2 }
|
||||
double := N.Mul(2)
|
||||
|
||||
value, verr := Right(5)
|
||||
fn, ferr := Right(double)
|
||||
@@ -83,7 +84,7 @@ func TestApV_ValueLeft_FunctionRight(t *testing.T) {
|
||||
sg := makeErrorConcatSemigroup()
|
||||
apv := ApV[int, int](sg)
|
||||
|
||||
double := func(x int) int { return x * 2 }
|
||||
double := N.Mul(2)
|
||||
|
||||
valueError := errors.New("invalid value")
|
||||
value, verr := Left[int](valueError)
|
||||
@@ -345,7 +346,7 @@ func BenchmarkApV_BothRight(b *testing.B) {
|
||||
sg := makeErrorConcatSemigroup()
|
||||
apv := ApV[int, int](sg)
|
||||
|
||||
double := func(x int) int { return x * 2 }
|
||||
double := N.Mul(2)
|
||||
value, verr := Right(5)
|
||||
fn, ferr := Right(double)
|
||||
|
||||
|
||||
@@ -141,6 +141,10 @@ func GetOrElse[E, A, HKTEA, HKTA any](mchain func(HKTEA, func(ET.Either[E, A]) H
|
||||
return MatchE(mchain, onLeft, mof)
|
||||
}
|
||||
|
||||
func GetOrElseOf[E, A, HKTEA, HKTA any](mchain func(HKTEA, func(ET.Either[E, A]) HKTA) HKTA, mof func(A) HKTA, onLeft func(E) A) func(HKTEA) HKTA {
|
||||
return MatchE(mchain, F.Flow2(onLeft, mof), mof)
|
||||
}
|
||||
|
||||
func OrElse[E1, E2, A, HKTE1A, HKTE2A any](mchain func(HKTE1A, func(ET.Either[E1, A]) HKTE2A) HKTE2A, mof func(ET.Either[E2, A]) HKTE2A, onLeft func(E1) HKTE2A) func(HKTE1A) HKTE2A {
|
||||
return MatchE(mchain, onLeft, F.Flow2(ET.Right[E2, A], mof))
|
||||
}
|
||||
|
||||
@@ -34,13 +34,16 @@ import (
|
||||
// Monads must satisfy the monad laws:
|
||||
//
|
||||
// Left Identity:
|
||||
// Chain(f)(Of(a)) == f(a)
|
||||
//
|
||||
// Chain(f)(Of(a)) == f(a)
|
||||
//
|
||||
// Right Identity:
|
||||
// Chain(Of)(m) == m
|
||||
//
|
||||
// Chain(Of)(m) == m
|
||||
//
|
||||
// Associativity:
|
||||
// Chain(g)(Chain(f)(m)) == Chain(x => Chain(g)(f(x)))(m)
|
||||
//
|
||||
// Chain(g)(Chain(f)(m)) == Chain(x => Chain(g)(f(x)))(m)
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The input value type
|
||||
@@ -50,20 +53,21 @@ import (
|
||||
// - HKTFAB: The higher-kinded type containing a function from A to B
|
||||
//
|
||||
// Example:
|
||||
// // Given a Monad for Option
|
||||
// var m Monad[int, string, Option[int], Option[string], Option[func(int) string]]
|
||||
//
|
||||
// // Use Of to create a value
|
||||
// value := m.Of(42) // Some(42)
|
||||
// // Given a Monad for Option
|
||||
// var m Monad[int, string, Option[int], Option[string], Option[func(int) string]]
|
||||
//
|
||||
// // Use Chain for dependent operations
|
||||
// chainFn := m.Chain(func(x int) Option[string] {
|
||||
// if x > 0 {
|
||||
// return Some(strconv.Itoa(x))
|
||||
// }
|
||||
// return None[string]()
|
||||
// })
|
||||
// result := chainFn(value) // Some("42")
|
||||
// // Use Of to create a value
|
||||
// value := m.Of(42) // Some(42)
|
||||
//
|
||||
// // Use Chain for dependent operations
|
||||
// chainFn := m.Chain(func(x int) Option[string] {
|
||||
// if x > 0 {
|
||||
// return Some(strconv.Itoa(x))
|
||||
// }
|
||||
// return None[string]()
|
||||
// })
|
||||
// result := chainFn(value) // Some("42")
|
||||
type Monad[A, B, HKTA, HKTB, HKTFAB any] interface {
|
||||
applicative.Applicative[A, B, HKTA, HKTB, HKTFAB]
|
||||
chain.Chainable[A, B, HKTA, HKTB, HKTFAB]
|
||||
|
||||
@@ -26,9 +26,10 @@ package pointed
|
||||
// - HKTA: The higher-kinded type containing A (e.g., Option[A], Either[E, A])
|
||||
//
|
||||
// Example:
|
||||
// // Given a pointed functor for Option[int]
|
||||
// var p Pointed[int, Option[int]]
|
||||
// result := p.Of(42) // Returns Some(42)
|
||||
//
|
||||
// // Given a pointed functor for Option[int]
|
||||
// var p Pointed[int, Option[int]]
|
||||
// result := p.Of(42) // Returns Some(42)
|
||||
type Pointed[A, HKTA any] interface {
|
||||
// Of lifts a pure value into its higher-kinded type context.
|
||||
//
|
||||
|
||||
@@ -68,6 +68,15 @@ func MonadTraverse[MA ~map[K]A, MB ~map[K]B, K comparable, A, B, HKTB, HKTAB, HK
|
||||
return traverseWithIndex(fof, fmap, fap, r, F.Ignore1of2[K](f))
|
||||
}
|
||||
|
||||
func MonadTraverseWithIndex[MA ~map[K]A, MB ~map[K]B, K comparable, A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func(MB) HKTRB,
|
||||
fmap func(func(MB) func(B) MB) func(HKTRB) HKTAB,
|
||||
fap func(HKTB) func(HKTAB) HKTRB,
|
||||
|
||||
r MA, f func(K, A) HKTB) HKTRB {
|
||||
return traverseWithIndex(fof, fmap, fap, r, f)
|
||||
}
|
||||
|
||||
func TraverseWithIndex[MA ~map[K]A, MB ~map[K]B, K comparable, A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func(MB) HKTRB,
|
||||
fmap func(func(MB) func(B) MB) func(HKTRB) HKTAB,
|
||||
|
||||
@@ -369,7 +369,7 @@ func TestMonadTypeClass(t *testing.T) {
|
||||
m.Chain(func(x int) IO[int] {
|
||||
return m.Of(x * 2)
|
||||
}),
|
||||
m.Map(func(x int) int { return x + 1 }),
|
||||
m.Map(N.Add(1)),
|
||||
)
|
||||
|
||||
assert.Equal(t, 43, result())
|
||||
|
||||
124
v2/io/logging.go
124
v2/io/logging.go
@@ -18,6 +18,10 @@ package io
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/template"
|
||||
|
||||
L "github.com/IBM/fp-go/v2/logging"
|
||||
)
|
||||
@@ -32,13 +36,14 @@ import (
|
||||
// io.ChainFirst(io.Logger[User]()("Fetched user")),
|
||||
// processUser,
|
||||
// )
|
||||
func Logger[A any](loggers ...*log.Logger) func(string) Kleisli[A, any] {
|
||||
func Logger[A any](loggers ...*log.Logger) func(string) Kleisli[A, A] {
|
||||
_, right := L.LoggingCallbacks(loggers...)
|
||||
return func(prefix string) Kleisli[A, any] {
|
||||
return func(a A) IO[any] {
|
||||
return FromImpure(func() {
|
||||
return func(prefix string) Kleisli[A, A] {
|
||||
return func(a A) IO[A] {
|
||||
return func() A {
|
||||
right("%s: %v", prefix, a)
|
||||
})
|
||||
return a
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,11 +58,12 @@ func Logger[A any](loggers ...*log.Logger) func(string) Kleisli[A, any] {
|
||||
// io.ChainFirst(io.Logf[User]("User: %+v")),
|
||||
// processUser,
|
||||
// )
|
||||
func Logf[A any](prefix string) Kleisli[A, any] {
|
||||
return func(a A) IO[any] {
|
||||
return FromImpure(func() {
|
||||
func Logf[A any](prefix string) Kleisli[A, A] {
|
||||
return func(a A) IO[A] {
|
||||
return func() A {
|
||||
log.Printf(prefix, a)
|
||||
})
|
||||
return a
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,10 +78,102 @@ func Logf[A any](prefix string) Kleisli[A, any] {
|
||||
// io.ChainFirst(io.Printf[User]("User: %+v\n")),
|
||||
// processUser,
|
||||
// )
|
||||
func Printf[A any](prefix string) Kleisli[A, any] {
|
||||
return func(a A) IO[any] {
|
||||
return FromImpure(func() {
|
||||
func Printf[A any](prefix string) Kleisli[A, A] {
|
||||
return func(a A) IO[A] {
|
||||
return func() A {
|
||||
fmt.Printf(prefix, a)
|
||||
})
|
||||
return a
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleLogging is a helper function that creates a Kleisli arrow for logging/printing
|
||||
// values using Go template syntax. It lazily compiles the template on first use and
|
||||
// executes it with the provided value as data.
|
||||
//
|
||||
// Parameters:
|
||||
// - onSuccess: callback function to handle successfully formatted output
|
||||
// - onError: callback function to handle template parsing or execution errors
|
||||
// - prefix: Go template string to format the value
|
||||
//
|
||||
// The template is compiled lazily using sync.Once to ensure it's only parsed once.
|
||||
// The function always returns the original value unchanged, making it suitable for
|
||||
// use with ChainFirst or similar operations.
|
||||
func handleLogging[A any](onSuccess func(string), onError func(error), prefix string) Kleisli[A, A] {
|
||||
var tmp *template.Template
|
||||
var err error
|
||||
var once sync.Once
|
||||
|
||||
init := func() {
|
||||
tmp, err = template.New("").Parse(prefix)
|
||||
}
|
||||
|
||||
return func(a A) IO[A] {
|
||||
return func() A {
|
||||
// make sure to compile lazily
|
||||
once.Do(init)
|
||||
if err == nil {
|
||||
var buffer strings.Builder
|
||||
tmpErr := tmp.Execute(&buffer, a)
|
||||
if tmpErr != nil {
|
||||
onError(tmpErr)
|
||||
onSuccess(fmt.Sprintf("%v", a))
|
||||
} else {
|
||||
onSuccess(buffer.String())
|
||||
}
|
||||
} else {
|
||||
onError(err)
|
||||
onSuccess(fmt.Sprintf("%v", a))
|
||||
}
|
||||
// in any case return the original value
|
||||
return a
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LogGo constructs a logger function using Go template syntax for formatting.
|
||||
// The prefix string is parsed as a Go template and executed with the value as data.
|
||||
// Both successful output and template errors are logged using log.Println.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
// result := pipe.Pipe2(
|
||||
// fetchUser(),
|
||||
// io.ChainFirst(io.LogGo[User]("User: {{.Name}}, Age: {{.Age}}")),
|
||||
// processUser,
|
||||
// )
|
||||
func LogGo[A any](prefix string) Kleisli[A, A] {
|
||||
return handleLogging[A](func(value string) {
|
||||
log.Println(value)
|
||||
}, func(err error) {
|
||||
log.Println(err)
|
||||
}, prefix)
|
||||
}
|
||||
|
||||
// PrintGo constructs a printer function using Go template syntax for formatting.
|
||||
// The prefix string is parsed as a Go template and executed with the value as data.
|
||||
// Successful output is printed to stdout using fmt.Println, while template errors
|
||||
// are printed to stderr using fmt.Fprintln.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
// result := pipe.Pipe2(
|
||||
// fetchUser(),
|
||||
// io.ChainFirst(io.PrintGo[User]("User: {{.Name}}, Age: {{.Age}}")),
|
||||
// processUser,
|
||||
// )
|
||||
func PrintGo[A any](prefix string) Kleisli[A, A] {
|
||||
return handleLogging[A](func(value string) {
|
||||
fmt.Println(value)
|
||||
}, func(err error) {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
}, prefix)
|
||||
}
|
||||
|
||||
@@ -16,25 +16,206 @@
|
||||
package io
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLogger(t *testing.T) {
|
||||
|
||||
l := Logger[int]()
|
||||
|
||||
lio := l("out")
|
||||
|
||||
assert.NotPanics(t, func() { lio(10)() })
|
||||
}
|
||||
|
||||
func TestLoggerWithCustomLogger(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
customLogger := log.New(&buf, "", 0)
|
||||
|
||||
l := Logger[int](customLogger)
|
||||
lio := l("test value")
|
||||
|
||||
result := lio(42)()
|
||||
|
||||
assert.Equal(t, 42, result)
|
||||
assert.Contains(t, buf.String(), "test value")
|
||||
assert.Contains(t, buf.String(), "42")
|
||||
}
|
||||
|
||||
func TestLoggerReturnsOriginalValue(t *testing.T) {
|
||||
type TestStruct struct {
|
||||
Name string
|
||||
Value int
|
||||
}
|
||||
|
||||
l := Logger[TestStruct]()
|
||||
lio := l("test")
|
||||
|
||||
input := TestStruct{Name: "test", Value: 100}
|
||||
result := lio(input)()
|
||||
|
||||
assert.Equal(t, input, result)
|
||||
}
|
||||
|
||||
func TestLogf(t *testing.T) {
|
||||
|
||||
l := Logf[int]
|
||||
|
||||
lio := l("Value is %d")
|
||||
|
||||
assert.NotPanics(t, func() { lio(10)() })
|
||||
}
|
||||
|
||||
func TestLogfReturnsOriginalValue(t *testing.T) {
|
||||
l := Logf[string]
|
||||
lio := l("String: %s")
|
||||
|
||||
input := "hello"
|
||||
result := lio(input)()
|
||||
|
||||
assert.Equal(t, input, result)
|
||||
}
|
||||
|
||||
func TestPrintfLogger(t *testing.T) {
|
||||
l := Printf[int]
|
||||
lio := l("Value: %d\n")
|
||||
assert.NotPanics(t, func() { lio(10)() })
|
||||
}
|
||||
|
||||
func TestPrintfLoggerReturnsOriginalValue(t *testing.T) {
|
||||
l := Printf[float64]
|
||||
lio := l("Number: %.2f\n")
|
||||
|
||||
input := 3.14159
|
||||
result := lio(input)()
|
||||
|
||||
assert.Equal(t, input, result)
|
||||
}
|
||||
|
||||
func TestLogGo(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
l := LogGo[User]
|
||||
lio := l("User: {{.Name}}, Age: {{.Age}}")
|
||||
|
||||
input := User{Name: "Alice", Age: 30}
|
||||
assert.NotPanics(t, func() { lio(input)() })
|
||||
}
|
||||
|
||||
func TestLogGoReturnsOriginalValue(t *testing.T) {
|
||||
type Product struct {
|
||||
ID int
|
||||
Name string
|
||||
Price float64
|
||||
}
|
||||
|
||||
l := LogGo[Product]
|
||||
lio := l("Product: {{.Name}} ({{.ID}})")
|
||||
|
||||
input := Product{ID: 123, Name: "Widget", Price: 19.99}
|
||||
result := lio(input)()
|
||||
|
||||
assert.Equal(t, input, result)
|
||||
}
|
||||
|
||||
func TestLogGoWithInvalidTemplate(t *testing.T) {
|
||||
l := LogGo[int]
|
||||
// Invalid template syntax
|
||||
lio := l("Value: {{.MissingField")
|
||||
|
||||
// Should not panic even with invalid template
|
||||
assert.NotPanics(t, func() { lio(42)() })
|
||||
}
|
||||
|
||||
func TestLogGoWithComplexTemplate(t *testing.T) {
|
||||
type Address struct {
|
||||
Street string
|
||||
City string
|
||||
}
|
||||
type Person struct {
|
||||
Name string
|
||||
Address Address
|
||||
}
|
||||
|
||||
l := LogGo[Person]
|
||||
lio := l("Person: {{.Name}} from {{.Address.City}}")
|
||||
|
||||
input := Person{
|
||||
Name: "Bob",
|
||||
Address: Address{Street: "Main St", City: "NYC"},
|
||||
}
|
||||
result := lio(input)()
|
||||
|
||||
assert.Equal(t, input, result)
|
||||
}
|
||||
|
||||
func TestPrintGo(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
l := PrintGo[User]
|
||||
lio := l("User: {{.Name}}, Age: {{.Age}}")
|
||||
|
||||
input := User{Name: "Charlie", Age: 25}
|
||||
assert.NotPanics(t, func() { lio(input)() })
|
||||
}
|
||||
|
||||
func TestPrintGoReturnsOriginalValue(t *testing.T) {
|
||||
type Score struct {
|
||||
Player string
|
||||
Points int
|
||||
}
|
||||
|
||||
l := PrintGo[Score]
|
||||
lio := l("{{.Player}}: {{.Points}} points")
|
||||
|
||||
input := Score{Player: "Alice", Points: 100}
|
||||
result := lio(input)()
|
||||
|
||||
assert.Equal(t, input, result)
|
||||
}
|
||||
|
||||
func TestPrintGoWithInvalidTemplate(t *testing.T) {
|
||||
l := PrintGo[string]
|
||||
// Invalid template syntax
|
||||
lio := l("Value: {{.}")
|
||||
|
||||
// Should not panic even with invalid template
|
||||
assert.NotPanics(t, func() { lio("test")() })
|
||||
}
|
||||
|
||||
func TestLogGoInPipeline(t *testing.T) {
|
||||
type Data struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
input := Data{Value: 10}
|
||||
|
||||
result := F.Pipe2(
|
||||
Of(input),
|
||||
ChainFirst(LogGo[Data]("Processing: {{.Value}}")),
|
||||
Map(func(d Data) Data {
|
||||
return Data{Value: d.Value * 2}
|
||||
}),
|
||||
)()
|
||||
|
||||
assert.Equal(t, 20, result.Value)
|
||||
}
|
||||
|
||||
func TestPrintGoInPipeline(t *testing.T) {
|
||||
input := "hello"
|
||||
|
||||
result := F.Pipe2(
|
||||
Of(input),
|
||||
ChainFirst(PrintGo[string]("Input: {{.}}")),
|
||||
Map(func(s string) string {
|
||||
return s + " world"
|
||||
}),
|
||||
)()
|
||||
|
||||
assert.Equal(t, "hello world", result)
|
||||
}
|
||||
|
||||
@@ -29,6 +29,48 @@ var (
|
||||
Create = ioeither.Eitherize1(os.Create)
|
||||
// ReadFile reads the context of a file
|
||||
ReadFile = ioeither.Eitherize1(os.ReadFile)
|
||||
// Stat returns [FileInfo] object
|
||||
Stat = ioeither.Eitherize1(os.Stat)
|
||||
|
||||
// UserCacheDir returns an [IOEither] that resolves to the default root directory
|
||||
// to use for user-specific cached data. Users should create their own application-specific
|
||||
// subdirectory within this one and use that.
|
||||
//
|
||||
// On Unix systems, it returns $XDG_CACHE_HOME as specified by
|
||||
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if
|
||||
// non-empty, else $HOME/.cache.
|
||||
// On Darwin, it returns $HOME/Library/Caches.
|
||||
// On Windows, it returns %LocalAppData%.
|
||||
// On Plan 9, it returns $home/lib/cache.
|
||||
//
|
||||
// If the location cannot be determined (for example, $HOME is not defined),
|
||||
// then it will return an error wrapped in [E.Left].
|
||||
UserCacheDir = ioeither.Eitherize0(os.UserCacheDir)()
|
||||
|
||||
// UserConfigDir returns an [IOEither] that resolves to the default root directory
|
||||
// to use for user-specific configuration data. Users should create their own
|
||||
// application-specific subdirectory within this one and use that.
|
||||
//
|
||||
// On Unix systems, it returns $XDG_CONFIG_HOME as specified by
|
||||
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if
|
||||
// non-empty, else $HOME/.config.
|
||||
// On Darwin, it returns $HOME/Library/Application Support.
|
||||
// On Windows, it returns %AppData%.
|
||||
// On Plan 9, it returns $home/lib.
|
||||
//
|
||||
// If the location cannot be determined (for example, $HOME is not defined),
|
||||
// then it will return an error wrapped in [E.Left].
|
||||
UserConfigDir = ioeither.Eitherize0(os.UserConfigDir)()
|
||||
|
||||
// UserHomeDir returns an [IOEither] that resolves to the current user's home directory.
|
||||
//
|
||||
// On Unix, including macOS, it returns the $HOME environment variable.
|
||||
// On Windows, it returns %USERPROFILE%.
|
||||
// On Plan 9, it returns the $home environment variable.
|
||||
//
|
||||
// If the location cannot be determined (for example, $HOME is not defined),
|
||||
// then it will return an error wrapped in [E.Left].
|
||||
UserHomeDir = ioeither.Eitherize0(os.UserHomeDir)()
|
||||
)
|
||||
|
||||
// WriteFile writes a data blob to a file
|
||||
|
||||
80
v2/ioeither/file/read.go
Normal file
80
v2/ioeither/file/read.go
Normal file
@@ -0,0 +1,80 @@
|
||||
// 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 file
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
)
|
||||
|
||||
// Read uses a generator function to create a stream, reads data from it using a provided
|
||||
// reader function, and ensures the stream is properly closed after reading.
|
||||
//
|
||||
// This function provides safe resource management for reading operations by:
|
||||
// 1. Acquiring a ReadCloser resource using the provided acquire function
|
||||
// 2. Applying a reader function to extract data from the resource
|
||||
// 3. Ensuring the resource is closed, even if an error occurs during reading
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The type of data to be read from the stream
|
||||
// - RD: The type of the ReadCloser resource (must implement io.ReadCloser)
|
||||
//
|
||||
// Parameters:
|
||||
// - acquire: An IOEither that produces the ReadCloser resource
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// A Kleisli function that takes a reader function (which transforms RD to R)
|
||||
// and returns an IOEither that produces the read result R or an error.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// "os"
|
||||
// "io"
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// "github.com/IBM/fp-go/v2/ioeither"
|
||||
// "github.com/IBM/fp-go/v2/ioeither/file"
|
||||
// )
|
||||
//
|
||||
// // Read first 10 bytes from a file
|
||||
// readFirst10 := func(f *os.File) ioeither.IOEither[error, []byte] {
|
||||
// return ioeither.TryCatchError(func() ([]byte, error) {
|
||||
// buf := make([]byte, 10)
|
||||
// n, err := f.Read(buf)
|
||||
// return buf[:n], err
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// file.Open("data.txt"),
|
||||
// file.Read[[]byte, *os.File],
|
||||
// )(readFirst10)
|
||||
//
|
||||
// data, err := result()
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// fmt.Printf("Read: %s\n", data)
|
||||
//
|
||||
// The Read function ensures that the file is closed even if the reading operation fails,
|
||||
// providing safe and composable resource management in a functional style.
|
||||
func Read[R any, RD io.ReadCloser](acquire IOEither[error, RD]) Kleisli[error, Kleisli[error, RD, R], R] {
|
||||
return ioeither.WithResource[R](
|
||||
acquire,
|
||||
Close[RD])
|
||||
}
|
||||
354
v2/ioeither/file/read_test.go
Normal file
354
v2/ioeither/file/read_test.go
Normal file
@@ -0,0 +1,354 @@
|
||||
// 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 file
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// mockReadCloser is a mock implementation of io.ReadCloser for testing
|
||||
type mockReadCloser struct {
|
||||
data []byte
|
||||
readErr error
|
||||
closeErr error
|
||||
readPos int
|
||||
closeCalled bool
|
||||
}
|
||||
|
||||
func (m *mockReadCloser) Read(p []byte) (n int, err error) {
|
||||
if m.readErr != nil {
|
||||
return 0, m.readErr
|
||||
}
|
||||
if m.readPos >= len(m.data) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n = copy(p, m.data[m.readPos:])
|
||||
m.readPos += n
|
||||
if m.readPos >= len(m.data) {
|
||||
return n, io.EOF
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (m *mockReadCloser) Close() error {
|
||||
m.closeCalled = true
|
||||
return m.closeErr
|
||||
}
|
||||
|
||||
// TestReadSuccessfulRead tests reading data successfully from a ReadCloser
|
||||
func TestReadSuccessfulRead(t *testing.T) {
|
||||
testData := []byte("Hello, World!")
|
||||
mock := &mockReadCloser{data: testData}
|
||||
|
||||
// Create an acquire function that returns our mock
|
||||
acquire := ioeither.Of[error](mock)
|
||||
|
||||
// Create a reader function that reads all data
|
||||
reader := func(rc *mockReadCloser) IOEither[error, []byte] {
|
||||
return ioeither.TryCatchError(func() ([]byte, error) {
|
||||
return io.ReadAll(rc)
|
||||
})
|
||||
}
|
||||
|
||||
// Execute the Read operation
|
||||
result := Read[[]byte](acquire)(reader)
|
||||
either := result()
|
||||
|
||||
// Assertions
|
||||
assert.True(t, E.IsRight(either))
|
||||
data := E.GetOrElse(func(error) []byte { return nil })(either)
|
||||
assert.Equal(t, testData, data)
|
||||
assert.True(t, mock.closeCalled, "Close should have been called")
|
||||
}
|
||||
|
||||
// TestReadWithRealFile tests reading from an actual file
|
||||
func TestReadWithRealFile(t *testing.T) {
|
||||
// Create a temporary file
|
||||
tmpFile, err := os.CreateTemp("", "read_test_*.txt")
|
||||
require.NoError(t, err)
|
||||
defer os.Remove(tmpFile.Name())
|
||||
|
||||
testContent := []byte("Test file content for Read function")
|
||||
_, err = tmpFile.Write(testContent)
|
||||
require.NoError(t, err)
|
||||
tmpFile.Close()
|
||||
|
||||
// Use Read to read the file
|
||||
reader := func(f *os.File) IOEither[error, []byte] {
|
||||
return ioeither.TryCatchError(func() ([]byte, error) {
|
||||
return io.ReadAll(f)
|
||||
})
|
||||
}
|
||||
|
||||
result := Read[[]byte](Open(tmpFile.Name()))(reader)
|
||||
either := result()
|
||||
|
||||
assert.True(t, E.IsRight(either))
|
||||
data := E.GetOrElse(func(error) []byte { return nil })(either)
|
||||
assert.Equal(t, testContent, data)
|
||||
}
|
||||
|
||||
// TestReadPartialRead tests reading only part of the data
|
||||
func TestReadPartialRead(t *testing.T) {
|
||||
testData := []byte("Hello, World! This is a longer message.")
|
||||
mock := &mockReadCloser{data: testData}
|
||||
|
||||
acquire := ioeither.Of[error](mock)
|
||||
|
||||
// Reader that only reads first 13 bytes
|
||||
reader := func(rc *mockReadCloser) IOEither[error, []byte] {
|
||||
return ioeither.TryCatchError(func() ([]byte, error) {
|
||||
buf := make([]byte, 13)
|
||||
n, err := rc.Read(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
return buf[:n], nil
|
||||
})
|
||||
}
|
||||
|
||||
result := Read[[]byte](acquire)(reader)
|
||||
either := result()
|
||||
|
||||
assert.True(t, E.IsRight(either))
|
||||
data := E.GetOrElse(func(error) []byte { return nil })(either)
|
||||
assert.Equal(t, []byte("Hello, World!"), data)
|
||||
assert.True(t, mock.closeCalled, "Close should have been called")
|
||||
}
|
||||
|
||||
// TestReadErrorDuringRead tests that errors during reading are propagated
|
||||
func TestReadErrorDuringRead(t *testing.T) {
|
||||
readError := errors.New("read error")
|
||||
mock := &mockReadCloser{
|
||||
data: []byte("data"),
|
||||
readErr: readError,
|
||||
}
|
||||
|
||||
acquire := ioeither.Of[error](mock)
|
||||
|
||||
reader := func(rc *mockReadCloser) IOEither[error, []byte] {
|
||||
return ioeither.TryCatchError(func() ([]byte, error) {
|
||||
return io.ReadAll(rc)
|
||||
})
|
||||
}
|
||||
|
||||
result := Read[[]byte](acquire)(reader)
|
||||
either := result()
|
||||
|
||||
assert.True(t, E.IsLeft(either))
|
||||
err := E.Fold(func(e error) error { return e }, func([]byte) error { return nil })(either)
|
||||
assert.Equal(t, readError, err)
|
||||
assert.True(t, mock.closeCalled, "Close should be called even on read error")
|
||||
}
|
||||
|
||||
// TestReadErrorDuringClose tests that errors during close are handled
|
||||
func TestReadErrorDuringClose(t *testing.T) {
|
||||
closeError := errors.New("close error")
|
||||
mock := &mockReadCloser{
|
||||
data: []byte("Hello"),
|
||||
closeErr: closeError,
|
||||
}
|
||||
|
||||
acquire := ioeither.Of[error](mock)
|
||||
|
||||
reader := func(rc *mockReadCloser) IOEither[error, []byte] {
|
||||
return ioeither.TryCatchError(func() ([]byte, error) {
|
||||
return io.ReadAll(rc)
|
||||
})
|
||||
}
|
||||
|
||||
result := Read[[]byte](acquire)(reader)
|
||||
either := result()
|
||||
|
||||
// The close error should be propagated
|
||||
assert.True(t, E.IsLeft(either))
|
||||
assert.True(t, mock.closeCalled, "Close should have been called")
|
||||
}
|
||||
|
||||
// TestReadErrorDuringAcquire tests that errors during resource acquisition are propagated
|
||||
func TestReadErrorDuringAcquire(t *testing.T) {
|
||||
acquireError := errors.New("acquire error")
|
||||
acquire := ioeither.Left[*mockReadCloser](acquireError)
|
||||
|
||||
reader := func(rc *mockReadCloser) IOEither[error, []byte] {
|
||||
return ioeither.TryCatchError(func() ([]byte, error) {
|
||||
return io.ReadAll(rc)
|
||||
})
|
||||
}
|
||||
|
||||
result := Read[[]byte](acquire)(reader)
|
||||
either := result()
|
||||
|
||||
assert.True(t, E.IsLeft(either))
|
||||
err := E.Fold(func(e error) error { return e }, func([]byte) error { return nil })(either)
|
||||
assert.Equal(t, acquireError, err)
|
||||
}
|
||||
|
||||
// TestReadWithStringReader tests reading and transforming to a different type
|
||||
func TestReadWithStringReader(t *testing.T) {
|
||||
testData := []byte("Hello, World!")
|
||||
mock := &mockReadCloser{data: testData}
|
||||
|
||||
acquire := ioeither.Of[error](mock)
|
||||
|
||||
// Reader that converts bytes to uppercase string
|
||||
reader := func(rc *mockReadCloser) IOEither[error, string] {
|
||||
return ioeither.TryCatchError(func() (string, error) {
|
||||
data, err := io.ReadAll(rc)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.ToUpper(string(data)), nil
|
||||
})
|
||||
}
|
||||
|
||||
result := Read[string](acquire)(reader)
|
||||
either := result()
|
||||
|
||||
assert.True(t, E.IsRight(either))
|
||||
str := E.GetOrElse(func(error) string { return "" })(either)
|
||||
assert.Equal(t, "HELLO, WORLD!", str)
|
||||
assert.True(t, mock.closeCalled, "Close should have been called")
|
||||
}
|
||||
|
||||
// TestReadComposition tests composing Read with other operations
|
||||
func TestReadComposition(t *testing.T) {
|
||||
testData := []byte("42")
|
||||
mock := &mockReadCloser{data: testData}
|
||||
|
||||
acquire := ioeither.Of[error](mock)
|
||||
|
||||
// Reader that parses the content as an integer
|
||||
reader := func(rc *mockReadCloser) IOEither[error, int] {
|
||||
return ioeither.TryCatchError(func() (int, error) {
|
||||
data, err := io.ReadAll(rc)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var num int
|
||||
// Simple parsing
|
||||
num = int(data[0]-'0')*10 + int(data[1]-'0')
|
||||
return num, nil
|
||||
})
|
||||
}
|
||||
|
||||
result := F.Pipe1(
|
||||
acquire,
|
||||
Read[int],
|
||||
)(reader)
|
||||
|
||||
either := result()
|
||||
|
||||
assert.True(t, E.IsRight(either))
|
||||
num := E.GetOrElse(func(error) int { return 0 })(either)
|
||||
assert.Equal(t, 42, num)
|
||||
assert.True(t, mock.closeCalled, "Close should have been called")
|
||||
}
|
||||
|
||||
// TestReadMultipleOperations tests that Read can be used multiple times
|
||||
func TestReadMultipleOperations(t *testing.T) {
|
||||
// Create a function that creates a new mock each time
|
||||
createMock := func() *mockReadCloser {
|
||||
return &mockReadCloser{data: []byte("test data")}
|
||||
}
|
||||
|
||||
reader := func(rc *mockReadCloser) IOEither[error, []byte] {
|
||||
return ioeither.TryCatchError(func() ([]byte, error) {
|
||||
return io.ReadAll(rc)
|
||||
})
|
||||
}
|
||||
|
||||
// First read
|
||||
mock1 := createMock()
|
||||
result1 := Read[[]byte](ioeither.Of[error](mock1))(reader)
|
||||
either1 := result1()
|
||||
|
||||
assert.True(t, E.IsRight(either1))
|
||||
data1 := E.GetOrElse(func(error) []byte { return nil })(either1)
|
||||
assert.Equal(t, []byte("test data"), data1)
|
||||
assert.True(t, mock1.closeCalled)
|
||||
|
||||
// Second read with a new mock
|
||||
mock2 := createMock()
|
||||
result2 := Read[[]byte](ioeither.Of[error](mock2))(reader)
|
||||
either2 := result2()
|
||||
|
||||
assert.True(t, E.IsRight(either2))
|
||||
data2 := E.GetOrElse(func(error) []byte { return nil })(either2)
|
||||
assert.Equal(t, []byte("test data"), data2)
|
||||
assert.True(t, mock2.closeCalled)
|
||||
}
|
||||
|
||||
// TestReadEnsuresCloseOnPanic tests that Close is called even if reader panics
|
||||
// Note: This is more of a conceptual test as the actual panic handling depends on
|
||||
// the implementation of WithResource
|
||||
func TestReadWithEmptyData(t *testing.T) {
|
||||
mock := &mockReadCloser{data: []byte{}}
|
||||
|
||||
acquire := ioeither.Of[error](mock)
|
||||
|
||||
reader := func(rc *mockReadCloser) IOEither[error, []byte] {
|
||||
return ioeither.TryCatchError(func() ([]byte, error) {
|
||||
return io.ReadAll(rc)
|
||||
})
|
||||
}
|
||||
|
||||
result := Read[[]byte](acquire)(reader)
|
||||
either := result()
|
||||
|
||||
assert.True(t, E.IsRight(either))
|
||||
data := E.GetOrElse(func(error) []byte { return nil })(either)
|
||||
assert.Empty(t, data)
|
||||
assert.True(t, mock.closeCalled, "Close should be called even with empty data")
|
||||
}
|
||||
|
||||
// TestReadIntegrationWithEither tests integration with Either operations
|
||||
func TestReadIntegrationWithEither(t *testing.T) {
|
||||
testData := []byte("integration test")
|
||||
mock := &mockReadCloser{data: testData}
|
||||
|
||||
acquire := ioeither.Of[error](mock)
|
||||
|
||||
reader := func(rc *mockReadCloser) IOEither[error, []byte] {
|
||||
return ioeither.TryCatchError(func() ([]byte, error) {
|
||||
return io.ReadAll(rc)
|
||||
})
|
||||
}
|
||||
|
||||
result := Read[[]byte](acquire)(reader)
|
||||
either := result()
|
||||
|
||||
// Test with Either operations
|
||||
assert.True(t, E.IsRight(either))
|
||||
|
||||
folded := E.Fold(
|
||||
func(err error) string { return "error: " + err.Error() },
|
||||
func(data []byte) string { return "success: " + string(data) },
|
||||
)(either)
|
||||
|
||||
assert.Equal(t, "success: integration test", folded)
|
||||
assert.True(t, mock.closeCalled)
|
||||
}
|
||||
@@ -31,7 +31,7 @@ import (
|
||||
func TestBuilderWithQuery(t *testing.T) {
|
||||
// add some query
|
||||
withLimit := R.WithQueryArg("limit")("10")
|
||||
withURL := R.WithUrl("http://www.example.org?a=b")
|
||||
withURL := R.WithURL("http://www.example.org?a=b")
|
||||
|
||||
b := F.Pipe2(
|
||||
R.Default,
|
||||
|
||||
@@ -264,6 +264,11 @@ func GetOrElse[E, A any](onLeft func(E) IO[A]) func(IOEither[E, A]) IO[A] {
|
||||
return eithert.GetOrElse(io.MonadChain[Either[E, A], A], io.MonadOf[A], onLeft)
|
||||
}
|
||||
|
||||
// GetOrElseOf extracts the value or maps the error
|
||||
func GetOrElseOf[E, A any](onLeft func(E) A) func(IOEither[E, A]) IO[A] {
|
||||
return eithert.GetOrElseOf(io.MonadChain[Either[E, A], A], io.MonadOf[A], onLeft)
|
||||
}
|
||||
|
||||
// MonadChainTo composes to the second monad ignoring the return value of the first
|
||||
func MonadChainTo[A, E, B any](fa IOEither[E, A], fb IOEither[E, B]) IOEither[E, B] {
|
||||
return MonadChain(fa, function.Constant1[A](fb))
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
func BenchmarkOf(b *testing.B) {
|
||||
@@ -29,7 +30,7 @@ func BenchmarkOf(b *testing.B) {
|
||||
|
||||
func BenchmarkMap(b *testing.B) {
|
||||
io := Of(42)
|
||||
f := func(x int) int { return x * 2 }
|
||||
f := N.Mul(2)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -68,9 +69,9 @@ func BenchmarkBind(b *testing.B) {
|
||||
}
|
||||
|
||||
func BenchmarkPipeline(b *testing.B) {
|
||||
f1 := func(x int) int { return x + 1 }
|
||||
f2 := func(x int) int { return x * 2 }
|
||||
f3 := func(x int) int { return x - 3 }
|
||||
f1 := N.Add(1)
|
||||
f2 := N.Mul(2)
|
||||
f3 := N.Sub(3)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -93,9 +94,9 @@ func BenchmarkExecute(b *testing.B) {
|
||||
}
|
||||
|
||||
func BenchmarkExecutePipeline(b *testing.B) {
|
||||
f1 := func(x int) int { return x + 1 }
|
||||
f2 := func(x int) int { return x * 2 }
|
||||
f3 := func(x int) int { return x - 3 }
|
||||
f1 := N.Add(1)
|
||||
f2 := N.Mul(2)
|
||||
f3 := N.Sub(3)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
||||
@@ -29,6 +29,48 @@ var (
|
||||
Create = file.Create
|
||||
// ReadFile reads the context of a file
|
||||
ReadFile = file.ReadFile
|
||||
// Stat returns [FileInfo] object
|
||||
Stat = file.Stat
|
||||
|
||||
// UserCacheDir returns an [IOResult] that resolves to the default root directory
|
||||
// to use for user-specific cached data. Users should create their own application-specific
|
||||
// subdirectory within this one and use that.
|
||||
//
|
||||
// On Unix systems, it returns $XDG_CACHE_HOME as specified by
|
||||
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if
|
||||
// non-empty, else $HOME/.cache.
|
||||
// On Darwin, it returns $HOME/Library/Caches.
|
||||
// On Windows, it returns %LocalAppData%.
|
||||
// On Plan 9, it returns $home/lib/cache.
|
||||
//
|
||||
// If the location cannot be determined (for example, $HOME is not defined),
|
||||
// then it will return an error wrapped in [Err].
|
||||
UserCacheDir = file.UserCacheDir
|
||||
|
||||
// UserConfigDir returns an [IOResult] that resolves to the default root directory
|
||||
// to use for user-specific configuration data. Users should create their own
|
||||
// application-specific subdirectory within this one and use that.
|
||||
//
|
||||
// On Unix systems, it returns $XDG_CONFIG_HOME as specified by
|
||||
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if
|
||||
// non-empty, else $HOME/.config.
|
||||
// On Darwin, it returns $HOME/Library/Application Support.
|
||||
// On Windows, it returns %AppData%.
|
||||
// On Plan 9, it returns $home/lib.
|
||||
//
|
||||
// If the location cannot be determined (for example, $HOME is not defined),
|
||||
// then it will return an error wrapped in [Err].
|
||||
UserConfigDir = file.UserConfigDir
|
||||
|
||||
// UserHomeDir returns an [IOResult] that resolves to the current user's home directory.
|
||||
//
|
||||
// On Unix, including macOS, it returns the $HOME environment variable.
|
||||
// On Windows, it returns %USERPROFILE%.
|
||||
// On Plan 9, it returns the $home environment variable.
|
||||
//
|
||||
// If the location cannot be determined (for example, $HOME is not defined),
|
||||
// then it will return an error wrapped in [Err].
|
||||
UserHomeDir = file.UserHomeDir
|
||||
)
|
||||
|
||||
// WriteFile writes a data blob to a file
|
||||
|
||||
111
v2/ioresult/file/read.go
Normal file
111
v2/ioresult/file/read.go
Normal file
@@ -0,0 +1,111 @@
|
||||
// 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 file
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/IBM/fp-go/v2/ioeither/file"
|
||||
)
|
||||
|
||||
// Read uses a generator function to create a stream, reads data from it using a provided
|
||||
// reader function, and ensures the stream is properly closed after reading.
|
||||
//
|
||||
// This function provides safe resource management for reading operations by:
|
||||
// 1. Acquiring a ReadCloser resource using the provided acquire function
|
||||
// 2. Applying a reader function to extract data from the resource
|
||||
// 3. Ensuring the resource is closed, even if an error occurs during reading
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The type of data to be read from the stream
|
||||
// - RD: The type of the ReadCloser resource (must implement io.ReadCloser)
|
||||
//
|
||||
// Parameters:
|
||||
// - acquire: An IOResult that produces the ReadCloser resource
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// A Kleisli function that takes a reader function (which transforms RD to R)
|
||||
// and returns an IOResult that produces the read result R or an error.
|
||||
//
|
||||
// The key difference from ioeither.Read is that this returns IOResult[R] which is
|
||||
// IO[Result[R]], representing a computation that returns a Result type (tuple of value and error)
|
||||
// rather than an Either type.
|
||||
//
|
||||
// Example - Reading first N bytes from a file:
|
||||
//
|
||||
// import (
|
||||
// "os"
|
||||
// "io"
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// R "github.com/IBM/fp-go/v2/result"
|
||||
// "github.com/IBM/fp-go/v2/ioresult"
|
||||
// "github.com/IBM/fp-go/v2/ioresult/file"
|
||||
// )
|
||||
//
|
||||
// // Read first 10 bytes from a file
|
||||
// readFirst10 := func(f *os.File) ioresult.IOResult[[]byte] {
|
||||
// return ioresult.TryCatch(func() ([]byte, error) {
|
||||
// buf := make([]byte, 10)
|
||||
// n, err := f.Read(buf)
|
||||
// return buf[:n], err
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// file.Open("data.txt"),
|
||||
// file.Read[[]byte, *os.File],
|
||||
// )(readFirst10)
|
||||
//
|
||||
// // Execute the IO operation to get the Result
|
||||
// res := result()
|
||||
// data, err := res() // Result is a tuple function
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// fmt.Printf("Read: %s\n", data)
|
||||
//
|
||||
// Example - Using with Result combinators:
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// file.Open("config.json"),
|
||||
// file.Read[[]byte, *os.File],
|
||||
// )(readFirst10)
|
||||
//
|
||||
// // Chain operations using Result combinators
|
||||
// processed := F.Pipe2(
|
||||
// result,
|
||||
// ioresult.Map(func(data []byte) string {
|
||||
// return string(data)
|
||||
// }),
|
||||
// ioresult.ChainFirst(func(s string) ioresult.IOResult[any] {
|
||||
// return ioresult.Of[any](fmt.Printf("Read: %s\n", s))
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
// res := processed()
|
||||
// str, err := res()
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// The Read function ensures that the file is closed even if the reading operation fails,
|
||||
// providing safe and composable resource management in a functional style.
|
||||
//
|
||||
//go:inline
|
||||
func Read[R any, RD io.ReadCloser](acquire IOResult[RD]) Kleisli[Kleisli[RD, R], R] {
|
||||
return file.Read[R](acquire)
|
||||
}
|
||||
64
v2/ioresult/file/read_example_test.go
Normal file
64
v2/ioresult/file/read_example_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
// 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 file_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
FL "github.com/IBM/fp-go/v2/file"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
I "github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/ioresult/file"
|
||||
)
|
||||
|
||||
// Example_read_basicUsage demonstrates basic usage of the Read function
|
||||
// to read data from a file with automatic resource cleanup.
|
||||
func Example_read_basicUsage() {
|
||||
// Create a temporary file for demonstration
|
||||
tmpFile, err := os.CreateTemp("", "example-*.txt")
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating temp file: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
|
||||
// Write some test data
|
||||
testData := "Hello, World! This is a test file."
|
||||
if _, err := tmpFile.WriteString(testData); err != nil {
|
||||
fmt.Printf("Error writing to temp file: %v\n", err)
|
||||
return
|
||||
}
|
||||
tmpFile.Close()
|
||||
|
||||
// Define a reader function that reads the full file content
|
||||
readAll := F.Flow2(
|
||||
FL.ToReader[*os.File],
|
||||
ioresult.Eitherize1(io.ReadAll),
|
||||
)
|
||||
|
||||
content := F.Pipe2(
|
||||
readAll,
|
||||
file.Read[[]byte](file.Open(tmpFile.Name())),
|
||||
ioresult.TapIOK(I.Printf[[]byte]("%s\n")),
|
||||
)
|
||||
|
||||
content()
|
||||
|
||||
// Output: Hello, World! This is a test file.
|
||||
}
|
||||
@@ -22,11 +22,110 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// CreateTemp created a temp file with proper parametrization
|
||||
// CreateTemp creates a temporary file with proper parametrization.
|
||||
// It is an alias for ioeither.file.CreateTemp which wraps os.CreateTemp
|
||||
// in an IOResult context for functional composition.
|
||||
//
|
||||
// This function takes a directory and pattern parameter and returns an IOResult
|
||||
// that produces a temporary file handle when executed.
|
||||
//
|
||||
// Parameters:
|
||||
// - dir: directory where the temporary file should be created (empty string uses default temp dir)
|
||||
// - pattern: filename pattern with optional '*' placeholder for random suffix
|
||||
//
|
||||
// Returns:
|
||||
// IOResult[*os.File] that when executed creates and returns a temporary file handle
|
||||
//
|
||||
// Example:
|
||||
// tempFile := CreateTemp("", "myapp-*.tmp")
|
||||
// result := tempFile()
|
||||
// file, err := E.UnwrapError(result)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// defer file.Close()
|
||||
CreateTemp = file.CreateTemp
|
||||
)
|
||||
|
||||
// WithTempFile creates a temporary filthen invokes a callback to create a resource based on the filthen close and remove the temp file
|
||||
// WithTempFile creates a temporary file, then invokes a callback to create a resource
|
||||
// based on the file, then automatically closes and removes the temp file.
|
||||
//
|
||||
// This function provides safe temporary file management by:
|
||||
// 1. Creating a temporary file with sensible defaults
|
||||
// 2. Passing the file handle to the provided callback function
|
||||
// 3. Ensuring the file is closed and removed, even if the callback fails
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of result produced by the callback function
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Kleisli function that takes a *os.File and returns an IOResult[A]
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// IOResult[A] that when executed creates a temp file, runs the callback,
|
||||
// and cleans up the file regardless of success or failure
|
||||
//
|
||||
// Example - Writing and reading from a temporary file:
|
||||
//
|
||||
// import (
|
||||
// "io"
|
||||
// "os"
|
||||
// E "github.com/IBM/fp-go/v2/either"
|
||||
// "github.com/IBM/fp-go/v2/ioresult"
|
||||
// "github.com/IBM/fp-go/v2/ioresult/file"
|
||||
// )
|
||||
//
|
||||
// // Write data to temp file and return the number of bytes written
|
||||
// writeToTemp := func(f *os.File) ioresult.IOResult[int] {
|
||||
// return ioresult.TryCatchError(func() (int, error) {
|
||||
// data := []byte("Hello, temporary world!")
|
||||
// return f.Write(data)
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// result := file.WithTempFile(writeToTemp)
|
||||
// bytesWritten, err := E.UnwrapError(result())
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// fmt.Printf("Wrote %d bytes to temporary file\n", bytesWritten)
|
||||
//
|
||||
// Example - Processing data through a temporary file:
|
||||
//
|
||||
// processData := func(data []byte) ioresult.IOResult[string] {
|
||||
// return file.WithTempFile(func(f *os.File) ioresult.IOResult[string] {
|
||||
// return ioresult.TryCatchError(func() (string, error) {
|
||||
// // Write data to temp file
|
||||
// if _, err := f.Write(data); err != nil {
|
||||
// return "", err
|
||||
// }
|
||||
//
|
||||
// // Seek back to beginning
|
||||
// if _, err := f.Seek(0, 0); err != nil {
|
||||
// return "", err
|
||||
// }
|
||||
//
|
||||
// // Read and process
|
||||
// processed, err := io.ReadAll(f)
|
||||
// if err != nil {
|
||||
// return "", err
|
||||
// }
|
||||
//
|
||||
// return strings.ToUpper(string(processed)), nil
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// result := processData([]byte("hello world"))
|
||||
// output, err := E.UnwrapError(result())
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// fmt.Println(output) // "HELLO WORLD"
|
||||
//
|
||||
// The temporary file is guaranteed to be cleaned up even if the callback function
|
||||
// panics or returns an error, providing safe resource management in a functional style.
|
||||
//
|
||||
//go:inline
|
||||
func WithTempFile[A any](f Kleisli[*os.File, A]) IOResult[A] {
|
||||
|
||||
@@ -32,7 +32,6 @@ func TestCreateTemp(t *testing.T) {
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
||||
assert.NotNil(t, file)
|
||||
|
||||
tmpPath := file.Name()
|
||||
@@ -49,7 +48,6 @@ func TestCreateTemp(t *testing.T) {
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
||||
assert.NotNil(t, file)
|
||||
|
||||
tmpPath := file.Name()
|
||||
@@ -68,7 +66,6 @@ func TestCreateTemp(t *testing.T) {
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
||||
assert.NotNil(t, file)
|
||||
|
||||
tmpPath := file.Name()
|
||||
@@ -95,7 +92,6 @@ func TestWithTempFile(t *testing.T) {
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
||||
assert.Equal(t, testData, returnedData)
|
||||
})
|
||||
|
||||
@@ -115,7 +111,6 @@ func TestWithTempFile(t *testing.T) {
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
||||
assert.Equal(t, tmpPath, path)
|
||||
|
||||
_, statErr := os.Stat(tmpPath)
|
||||
@@ -165,7 +160,6 @@ func TestWithTempFile(t *testing.T) {
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
||||
assert.Equal(t, testContent, content)
|
||||
})
|
||||
|
||||
@@ -182,9 +176,8 @@ func TestWithTempFile(t *testing.T) {
|
||||
result := WithTempFile(useFile)()
|
||||
path, err := E.UnwrapError(result)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
||||
paths = append(paths, path)
|
||||
}
|
||||
|
||||
@@ -239,7 +232,6 @@ func TestWithTempFile(t *testing.T) {
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
||||
assert.Equal(t, testData, returnedData)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,9 +1,88 @@
|
||||
// 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 file
|
||||
|
||||
import "github.com/IBM/fp-go/v2/ioresult"
|
||||
|
||||
type (
|
||||
IOResult[T any] = ioresult.IOResult[T]
|
||||
Kleisli[A, B any] = ioresult.Kleisli[A, B]
|
||||
// IOResult represents a synchronous computation that may fail, returning a Result type.
|
||||
// It is an alias for ioresult.IOResult[T] which is equivalent to IO[Result[T]].
|
||||
//
|
||||
// IOResult[T] is a function that when executed returns Result[T], which is Either[error, T].
|
||||
// This provides a functional approach to handling IO operations that may fail, with
|
||||
// automatic resource management and composable error handling.
|
||||
//
|
||||
// Example:
|
||||
// var readFile IOResult[[]byte] = func() Result[[]byte] {
|
||||
// data, err := os.ReadFile("config.json")
|
||||
// return result.TryCatchError(data, err)
|
||||
// }
|
||||
//
|
||||
// // Execute the IO operation
|
||||
// result := readFile()
|
||||
// data, err := E.UnwrapError(result)
|
||||
IOResult[T any] = ioresult.IOResult[T]
|
||||
|
||||
// Kleisli represents a function that takes a value of type A and returns an IOResult[B].
|
||||
// It is an alias for ioresult.Kleisli[A, B] which is equivalent to Reader[A, IOResult[B]].
|
||||
//
|
||||
// Kleisli functions are the building blocks of monadic composition in the IOResult context.
|
||||
// They allow for chaining operations that may fail while maintaining functional purity.
|
||||
//
|
||||
// Example:
|
||||
// // A Kleisli function that reads from a file handle
|
||||
// var readAll Kleisli[*os.File, []byte] = func(f *os.File) IOResult[[]byte] {
|
||||
// return TryCatchError(func() ([]byte, error) {
|
||||
// return io.ReadAll(f)
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// // Can be composed with other Kleisli functions
|
||||
// var processFile = F.Pipe1(
|
||||
// file.Open("data.txt"),
|
||||
// file.Read[[]byte, *os.File],
|
||||
// )(readAll)
|
||||
Kleisli[A, B any] = ioresult.Kleisli[A, B]
|
||||
|
||||
// Operator represents a function that transforms one IOResult into another.
|
||||
// It is an alias for ioresult.Operator[A, B] which is equivalent to Kleisli[IOResult[A], B].
|
||||
//
|
||||
// Operators are used for transforming and composing IOResult values, providing a way
|
||||
// to build complex data processing pipelines while maintaining error handling semantics.
|
||||
//
|
||||
// Example:
|
||||
// // An operator that converts bytes to string
|
||||
// var bytesToString Operator[[]byte, string] = Map(func(data []byte) string {
|
||||
// return string(data)
|
||||
// })
|
||||
//
|
||||
// // An operator that validates JSON
|
||||
// var validateJSON Operator[string, map[string]interface{}] = ChainEitherK(
|
||||
// func(s string) Result[map[string]interface{}] {
|
||||
// var result map[string]interface{}
|
||||
// err := json.Unmarshal([]byte(s), &result)
|
||||
// return result.TryCatchError(result, err)
|
||||
// },
|
||||
// )
|
||||
//
|
||||
// // Compose operators in a pipeline
|
||||
// var processJSON = F.Pipe2(
|
||||
// readFileOperation,
|
||||
// bytesToString,
|
||||
// validateJSON,
|
||||
// )
|
||||
Operator[A, B any] = ioresult.Operator[A, B]
|
||||
)
|
||||
|
||||
@@ -18,6 +18,8 @@ package ioresult
|
||||
import (
|
||||
"time"
|
||||
|
||||
IOI "github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
RI "github.com/IBM/fp-go/v2/idiomatic/result"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
IOO "github.com/IBM/fp-go/v2/iooption"
|
||||
@@ -25,6 +27,19 @@ import (
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
func fromIOResultKleisliI[A, B any](f IOI.Kleisli[A, B]) Kleisli[A, B] {
|
||||
return func(a A) IOResult[B] {
|
||||
r := f(a)
|
||||
return func() Result[B] {
|
||||
return result.TryCatchError(r())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fromResultKleisliI[A, B any](f RI.Kleisli[A, B]) result.Kleisli[A, B] {
|
||||
return result.Eitherize1(f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Left[A any](l error) IOResult[A] {
|
||||
return ioeither.Left[A](l)
|
||||
@@ -65,6 +80,16 @@ func FromResult[A any](e Result[A]) IOResult[A] {
|
||||
return ioeither.FromEither(e)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FromEitherI[A any](a A, err error) IOResult[A] {
|
||||
return FromEither(result.TryCatchError(a, err))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FromResultI[A any](a A, err error) IOResult[A] {
|
||||
return FromEitherI(a, err)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FromOption[A any](onNone func() error) func(o O.Option[A]) IOResult[A] {
|
||||
return ioeither.FromOption[A](onNone)
|
||||
@@ -76,7 +101,7 @@ func FromIOOption[A any](onNone func() error) func(o IOO.IOOption[A]) IOResult[A
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainOptionK[A, B any](onNone func() error) func(func(A) O.Option[B]) Operator[A, B] {
|
||||
func ChainOptionK[A, B any](onNone func() error) func(O.Kleisli[A, B]) Operator[A, B] {
|
||||
return ioeither.ChainOptionK[A, B](onNone)
|
||||
}
|
||||
|
||||
@@ -139,6 +164,16 @@ func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||
return ioeither.Chain(f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadChainI[A, B any](fa IOResult[A], f IOI.Kleisli[A, B]) IOResult[B] {
|
||||
return ioeither.MonadChain(fa, fromIOResultKleisliI(f))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainI[A, B any](f IOI.Kleisli[A, B]) Operator[A, B] {
|
||||
return ioeither.Chain(fromIOResultKleisliI(f))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadChainEitherK[A, B any](ma IOResult[A], f result.Kleisli[A, B]) IOResult[B] {
|
||||
return ioeither.MonadChainEitherK(ma, f)
|
||||
@@ -251,6 +286,11 @@ func GetOrElse[A any](onLeft func(error) IO[A]) func(IOResult[A]) IO[A] {
|
||||
return ioeither.GetOrElse(onLeft)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func GetOrElseOf[A any](onLeft func(error) A) func(IOResult[A]) IO[A] {
|
||||
return ioeither.GetOrElseOf(onLeft)
|
||||
}
|
||||
|
||||
// MonadChainTo composes to the second monad ignoring the return value of the first
|
||||
//
|
||||
//go:inline
|
||||
|
||||
@@ -476,7 +476,7 @@ func Flatten[A any](mma Seq[Seq[A]]) Seq[A] {
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// fns := From(N.Mul(2), func(x int) int { return x + 10 })
|
||||
// fns := From(N.Mul(2), N.Add(10))
|
||||
// vals := From(5, 3)
|
||||
// result := MonadAp(fns, vals)
|
||||
// // yields: 10, 6, 15, 13 (each function applied to each value)
|
||||
@@ -492,7 +492,7 @@ func MonadAp[B, A any](fab Seq[func(A) B], fa Seq[A]) Seq[B] {
|
||||
// Example:
|
||||
//
|
||||
// applyTo5 := Ap(From(5))
|
||||
// fns := From(N.Mul(2), func(x int) int { return x + 10 })
|
||||
// fns := From(N.Mul(2), N.Add(10))
|
||||
// result := applyTo5(fns)
|
||||
// // yields: 10, 15
|
||||
//
|
||||
@@ -799,7 +799,7 @@ func FoldMapWithKey[K, A, B any](m M.Monoid[B]) func(func(K, A) B) func(Seq2[K,
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// fns := From(N.Mul(2), func(x int) int { return x + 10 })
|
||||
// fns := From(N.Mul(2), N.Add(10))
|
||||
// result := MonadFlap(fns, 5)
|
||||
// // yields: 10, 15
|
||||
//
|
||||
|
||||
@@ -251,7 +251,7 @@ func TestFlatten(t *testing.T) {
|
||||
func TestMonadAp(t *testing.T) {
|
||||
fns := From(
|
||||
N.Mul(2),
|
||||
func(x int) int { return x + 10 },
|
||||
N.Add(10),
|
||||
)
|
||||
vals := From(1, 2)
|
||||
result := MonadAp(fns, vals)
|
||||
@@ -261,7 +261,7 @@ func TestMonadAp(t *testing.T) {
|
||||
func TestAp(t *testing.T) {
|
||||
fns := From(
|
||||
N.Mul(2),
|
||||
func(x int) int { return x + 10 },
|
||||
N.Add(10),
|
||||
)
|
||||
vals := From(1, 2)
|
||||
applier := Ap[int](vals)
|
||||
@@ -425,7 +425,7 @@ func TestFoldMapWithKey(t *testing.T) {
|
||||
func TestMonadFlap(t *testing.T) {
|
||||
fns := From(
|
||||
N.Mul(2),
|
||||
func(x int) int { return x + 10 },
|
||||
N.Add(10),
|
||||
)
|
||||
result := MonadFlap(fns, 5)
|
||||
assert.Equal(t, []int{10, 15}, toSlice(result))
|
||||
@@ -434,7 +434,7 @@ func TestMonadFlap(t *testing.T) {
|
||||
func TestFlap(t *testing.T) {
|
||||
fns := From(
|
||||
N.Mul(2),
|
||||
func(x int) int { return x + 10 },
|
||||
N.Add(10),
|
||||
)
|
||||
flapper := Flap[int](5)
|
||||
result := toSlice(flapper(fns))
|
||||
@@ -525,7 +525,7 @@ func TestPipelineComposition(t *testing.T) {
|
||||
result := F.Pipe4(
|
||||
From(1, 2, 3, 4, 5, 6),
|
||||
Filter(func(x int) bool { return x%2 == 0 }),
|
||||
Map(func(x int) int { return x * 10 }),
|
||||
Map(N.Mul(10)),
|
||||
Prepend(0),
|
||||
toSlice[int],
|
||||
)
|
||||
|
||||
@@ -496,7 +496,7 @@ func TestMapComposition(t *testing.T) {
|
||||
result := F.Pipe3(
|
||||
Of(5),
|
||||
Map(N.Mul(2)),
|
||||
Map(func(x int) int { return x + 10 }),
|
||||
Map(N.Add(10)),
|
||||
Map(func(x int) int { return x }),
|
||||
)
|
||||
|
||||
|
||||
@@ -239,7 +239,7 @@ func TestFromZeroWithCompose(t *testing.T) {
|
||||
return O.MonadMap(opt, N.Mul(2))
|
||||
},
|
||||
func(opt O.Option[int]) O.Option[int] {
|
||||
return O.MonadMap(opt, func(x int) int { return x / 2 })
|
||||
return O.MonadMap(opt, N.Div(2))
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
package lens
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
EQ "github.com/IBM/fp-go/v2/eq"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
@@ -597,3 +599,11 @@ func IMap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](ab AB, ba BA) Operator[
|
||||
return MakeLensCurried(F.Flow2(ea.Get, ab), F.Flow2(ba, ea.Set))
|
||||
}
|
||||
}
|
||||
|
||||
func (l Lens[S, T]) String() string {
|
||||
return "Lens"
|
||||
}
|
||||
|
||||
func (l Lens[S, T]) Format(f fmt.State, c rune) {
|
||||
fmt.Fprint(f, l.String())
|
||||
}
|
||||
|
||||
@@ -557,7 +557,7 @@ func TestModifyLaws(t *testing.T) {
|
||||
// Modify composition: Modify(f ∘ g) = Modify(f) ∘ Modify(g)
|
||||
t.Run("ModifyComposition", func(t *testing.T) {
|
||||
f := N.Mul(2)
|
||||
g := func(x int) int { return x + 3 }
|
||||
g := N.Add(3)
|
||||
|
||||
// Modify(f ∘ g)
|
||||
composed := F.Flow2(g, f)
|
||||
|
||||
@@ -274,7 +274,7 @@ func TestFromIsoModify(t *testing.T) {
|
||||
t.Run("ModifyNoneToSome", func(t *testing.T) {
|
||||
config := Config{timeout: 0, retries: 3}
|
||||
// Map None to Some(10)
|
||||
modified := L.Modify[Config](O.Map(func(x int) int { return x + 10 }))(optTimeoutLens)(config)
|
||||
modified := L.Modify[Config](O.Map(N.Add(10)))(optTimeoutLens)(config)
|
||||
// Since it was None, Map doesn't apply, stays None (0)
|
||||
assert.Equal(t, 0, modified.timeout)
|
||||
})
|
||||
|
||||
@@ -31,7 +31,7 @@ type (
|
||||
// by applying a function that takes and returns the same type.
|
||||
//
|
||||
// Example:
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// increment := N.Add(1)
|
||||
// // increment is an Endomorphism[int]
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
package optional
|
||||
|
||||
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"
|
||||
@@ -27,6 +29,7 @@ import (
|
||||
type Optional[S, A any] struct {
|
||||
GetOption func(s S) O.Option[A]
|
||||
Set func(a A) EM.Endomorphism[S]
|
||||
name string
|
||||
}
|
||||
|
||||
// setCopy wraps a setter for a pointer into a setter that first creates a copy before
|
||||
@@ -41,29 +44,42 @@ func setCopy[SET ~func(*S, A) *S, S, A any](setter SET) func(s *S, a A) *S {
|
||||
// MakeOptional creates an Optional based on a getter and a setter function. Make sure that the setter creates a (shallow) copy of the
|
||||
// data. This happens automatically if the data is passed by value. For pointers consider to use `MakeOptionalRef`
|
||||
// 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] {
|
||||
return Optional[S, A]{GetOption: get, Set: F.Bind2of2(set)}
|
||||
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] {
|
||||
return Optional[S, A]{GetOption: get, Set: F.Bind2of2(set), name: name}
|
||||
}
|
||||
|
||||
// MakeOptionalRef creates an Optional based on a getter and a setter function. The setter passed in does not have to create a shallow
|
||||
// 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] {
|
||||
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] {
|
||||
return MakeOptionalWithName(get, setCopy(set), name)
|
||||
}
|
||||
|
||||
// Id returns am optional implementing the identity operation
|
||||
func id[S any](creator func(get func(S) O.Option[S], set func(S, S) S) Optional[S, S]) Optional[S, S] {
|
||||
return creator(O.Some[S], F.Second[S, S])
|
||||
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] {
|
||||
return creator(O.Some[S], F.Second[S, S], name)
|
||||
}
|
||||
|
||||
// Id returns am optional implementing the identity operation
|
||||
func Id[S any]() Optional[S, S] {
|
||||
return id(MakeOptional[S, S])
|
||||
return idWithName(MakeOptionalWithName[S, S], "Identity")
|
||||
}
|
||||
|
||||
// Id returns am optional implementing the identity operation
|
||||
func IdRef[S any]() Optional[*S, *S] {
|
||||
return id(MakeOptionalRef[S, *S])
|
||||
return idWithName(MakeOptionalRefWithName[S, *S], "Identity")
|
||||
}
|
||||
|
||||
func optionalModifyOption[S, A any](f func(A) A, optional Optional[S, A], s S) O.Option[S] {
|
||||
@@ -189,3 +205,11 @@ func IChainAny[S, A any]() func(Optional[S, any]) Optional[S, A] {
|
||||
return ichain(sa, fromAny, toAny)
|
||||
}
|
||||
}
|
||||
|
||||
func (l Optional[S, T]) String() string {
|
||||
return l.name
|
||||
}
|
||||
|
||||
func (l Optional[S, T]) Format(f fmt.State, c rune) {
|
||||
fmt.Fprint(f, l.String())
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ func AsOptional[S, A any](sa P.Prism[S, A]) OPT.Optional[S, A] {
|
||||
}
|
||||
|
||||
func PrismSome[A any]() P.Prism[O.Option[A], A] {
|
||||
return P.MakePrism(F.Identity[O.Option[A]], O.Some[A])
|
||||
return P.MakePrismWithName(F.Identity[O.Option[A]], O.Some[A], "PrismSome")
|
||||
}
|
||||
|
||||
// Some returns a `Optional` from a `Optional` focused on the `Some` of a `Option` type.
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
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"
|
||||
@@ -48,33 +50,19 @@ type (
|
||||
// },
|
||||
// func(v int) Result { return Success{Value: v} },
|
||||
// )
|
||||
Prism[S, A any] interface {
|
||||
Prism[S, A any] struct {
|
||||
// GetOption attempts to extract a value of type A from S.
|
||||
// Returns Some(a) if the extraction succeeds, None otherwise.
|
||||
GetOption(s S) Option[A]
|
||||
GetOption O.Kleisli[S, A]
|
||||
|
||||
// ReverseGet constructs an S from an A.
|
||||
// This operation always succeeds.
|
||||
ReverseGet(a A) S
|
||||
}
|
||||
ReverseGet func(A) S
|
||||
|
||||
// prismImpl is the internal implementation of the Prism interface.
|
||||
prismImpl[S, A any] struct {
|
||||
get func(S) Option[A]
|
||||
rev func(A) S
|
||||
name string
|
||||
}
|
||||
)
|
||||
|
||||
// GetOption implements the Prism interface for prismImpl.
|
||||
func (prism prismImpl[S, A]) GetOption(s S) Option[A] {
|
||||
return prism.get(s)
|
||||
}
|
||||
|
||||
// ReverseGet implements the Prism interface for prismImpl.
|
||||
func (prism prismImpl[S, A]) ReverseGet(a A) S {
|
||||
return prism.rev(a)
|
||||
}
|
||||
|
||||
// MakePrism constructs a Prism from GetOption and ReverseGet functions.
|
||||
//
|
||||
// Parameters:
|
||||
@@ -90,8 +78,15 @@ func (prism prismImpl[S, A]) ReverseGet(a A) S {
|
||||
// func(opt Option[int]) Option[int] { return opt },
|
||||
// func(n int) Option[int] { return Some(n) },
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func MakePrism[S, A any](get func(S) Option[A], rev func(A) S) Prism[S, A] {
|
||||
return prismImpl[S, A]{get, rev}
|
||||
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] {
|
||||
return Prism[S, A]{get, rev, name}
|
||||
}
|
||||
|
||||
// Id returns an identity prism that focuses on the entire value.
|
||||
@@ -106,7 +101,7 @@ func MakePrism[S, A any](get func(S) Option[A], rev func(A) S) Prism[S, A] {
|
||||
// value := idPrism.GetOption(42) // Some(42)
|
||||
// result := idPrism.ReverseGet(42) // 42
|
||||
func Id[S any]() Prism[S, S] {
|
||||
return MakePrism(O.Some[S], F.Identity[S])
|
||||
return MakePrismWithName(O.Some[S], F.Identity[S], "PrismIdentity")
|
||||
}
|
||||
|
||||
// FromPredicate creates a prism that matches values satisfying a predicate.
|
||||
@@ -125,7 +120,7 @@ func Id[S any]() Prism[S, S] {
|
||||
// value := positivePrism.GetOption(42) // Some(42)
|
||||
// value = positivePrism.GetOption(-5) // None[int]
|
||||
func FromPredicate[S any](pred func(S) bool) Prism[S, S] {
|
||||
return MakePrism(O.FromPredicate(pred), F.Identity[S])
|
||||
return MakePrismWithName(O.FromPredicate(pred), F.Identity[S], "PrismWithPredicate")
|
||||
}
|
||||
|
||||
// Compose composes two prisms to create a prism that focuses deeper into a structure.
|
||||
@@ -149,13 +144,15 @@ func FromPredicate[S any](pred func(S) bool) Prism[S, S] {
|
||||
// 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] {
|
||||
return func(sa Prism[S, A]) Prism[S, B] {
|
||||
return MakePrism(F.Flow2(
|
||||
return MakePrismWithName(F.Flow2(
|
||||
sa.GetOption,
|
||||
O.Chain(ab.GetOption),
|
||||
), F.Flow2(
|
||||
ab.ReverseGet,
|
||||
sa.ReverseGet,
|
||||
))
|
||||
),
|
||||
fmt.Sprintf("PrismCompose[%s x %s]", ab, sa),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,7 +210,7 @@ func Set[S, A any](a A) func(Prism[S, A]) EM.Endomorphism[S] {
|
||||
// prismSome creates a prism that focuses on the Some variant of an Option.
|
||||
// This is an internal helper used by the Some function.
|
||||
func prismSome[A any]() Prism[Option[A], A] {
|
||||
return MakePrism(F.Identity[Option[A]], O.Some[A])
|
||||
return MakePrismWithName(F.Identity[Option[A]], O.Some[A], "PrismSome")
|
||||
}
|
||||
|
||||
// Some creates a prism that focuses on the Some variant of an Option within a structure.
|
||||
@@ -242,9 +239,10 @@ func Some[S, A any](soa Prism[S, Option[A]]) Prism[S, A] {
|
||||
|
||||
// imap is an internal helper that bidirectionally maps a prism's focus type.
|
||||
func imap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](sa Prism[S, A], ab AB, ba BA) Prism[S, B] {
|
||||
return MakePrism(
|
||||
return MakePrismWithName(
|
||||
F.Flow2(sa.GetOption, O.Map(ab)),
|
||||
F.Flow2(ba, sa.ReverseGet),
|
||||
fmt.Sprintf("PrismIMap[%s]", sa),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -278,3 +276,11 @@ func IMap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](ab AB, ba BA) func(Pris
|
||||
return imap(sa, ab, ba)
|
||||
}
|
||||
}
|
||||
|
||||
func (l Prism[S, T]) String() string {
|
||||
return "Prism"
|
||||
}
|
||||
|
||||
func (l Prism[S, T]) Format(f fmt.State, c rune) {
|
||||
fmt.Fprint(f, l.String())
|
||||
}
|
||||
|
||||
@@ -17,8 +17,10 @@ package prism
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
@@ -67,10 +69,12 @@ import (
|
||||
// - Validating and transforming base64 data in pipelines
|
||||
// - Using different encodings (Standard, URL-safe, RawStd, RawURL)
|
||||
func FromEncoding(enc *base64.Encoding) Prism[string, []byte] {
|
||||
return MakePrism(F.Flow2(
|
||||
return MakePrismWithName(F.Flow2(
|
||||
either.Eitherize1(enc.DecodeString),
|
||||
either.Fold(F.Ignore1of1[error](option.None[[]byte]), option.Some),
|
||||
), enc.EncodeToString)
|
||||
), enc.EncodeToString,
|
||||
"PrismFromEncoding",
|
||||
)
|
||||
}
|
||||
|
||||
// ParseURL creates a prism for parsing and formatting URLs.
|
||||
@@ -114,10 +118,12 @@ func FromEncoding(enc *base64.Encoding) Prism[string, []byte] {
|
||||
// - Transforming URL strings in data pipelines
|
||||
// - Extracting and modifying URL components safely
|
||||
func ParseURL() Prism[string, *url.URL] {
|
||||
return MakePrism(F.Flow2(
|
||||
return MakePrismWithName(F.Flow2(
|
||||
either.Eitherize1(url.Parse),
|
||||
either.Fold(F.Ignore1of1[error](option.None[*url.URL]), option.Some),
|
||||
), (*url.URL).String)
|
||||
), (*url.URL).String,
|
||||
"PrismParseURL",
|
||||
)
|
||||
}
|
||||
|
||||
// InstanceOf creates a prism for type assertions on interface{}/any values.
|
||||
@@ -161,7 +167,8 @@ func ParseURL() Prism[string, *url.URL] {
|
||||
// - Type-safe deserialization and validation
|
||||
// - Pattern matching on interface{} values
|
||||
func InstanceOf[T any]() Prism[any, T] {
|
||||
return MakePrism(option.ToType[T], F.ToAny[T])
|
||||
var t T
|
||||
return MakePrismWithName(option.ToType[T], F.ToAny[T], fmt.Sprintf("PrismInstanceOf[%T]", t))
|
||||
}
|
||||
|
||||
// ParseDate creates a prism for parsing and formatting dates with a specific layout.
|
||||
@@ -212,10 +219,12 @@ func InstanceOf[T any]() Prism[any, T] {
|
||||
// - Converting between date formats
|
||||
// - Safely handling user-provided date inputs
|
||||
func ParseDate(layout string) Prism[string, time.Time] {
|
||||
return MakePrism(F.Flow2(
|
||||
return MakePrismWithName(F.Flow2(
|
||||
F.Bind1st(either.Eitherize2(time.Parse), layout),
|
||||
either.Fold(F.Ignore1of1[error](option.None[time.Time]), option.Some),
|
||||
), F.Bind2nd(time.Time.Format, layout))
|
||||
), F.Bind2nd(time.Time.Format, layout),
|
||||
"PrismParseDate",
|
||||
)
|
||||
}
|
||||
|
||||
// Deref creates a prism for safely dereferencing pointers.
|
||||
@@ -263,7 +272,7 @@ func ParseDate(layout string) Prism[string, time.Time] {
|
||||
// - Filtering out nil values in data pipelines
|
||||
// - Working with database nullable columns
|
||||
func Deref[T any]() Prism[*T, *T] {
|
||||
return MakePrism(option.FromNillable[T], F.Identity[*T])
|
||||
return MakePrismWithName(option.FromNillable[T], F.Identity[*T], "PrismDeref")
|
||||
}
|
||||
|
||||
// FromEither creates a prism for extracting Right values from Either types.
|
||||
@@ -309,7 +318,7 @@ func Deref[T any]() Prism[*T, *T] {
|
||||
// - Working with fallible operations
|
||||
// - Composing with other prisms for complex error handling
|
||||
func FromEither[E, T any]() Prism[Either[E, T], T] {
|
||||
return MakePrism(either.ToOption[E, T], either.Of[E, T])
|
||||
return MakePrismWithName(either.ToOption[E, T], either.Of[E, T], "PrismFromEither")
|
||||
}
|
||||
|
||||
// FromZero creates a prism that matches zero values of comparable types.
|
||||
@@ -352,11 +361,50 @@ func FromEither[E, T any]() Prism[Either[E, T], T] {
|
||||
// - Working with optional fields that use zero as "not set"
|
||||
// - Replacing zero values with defaults
|
||||
func FromZero[T comparable]() Prism[T, T] {
|
||||
return MakePrism(option.FromZero[T](), F.Identity[T])
|
||||
return MakePrismWithName(option.FromZero[T](), F.Identity[T], "PrismFromZero")
|
||||
}
|
||||
|
||||
// FromNonZero creates a prism that matches non-zero values of comparable types.
|
||||
// It provides a safe way to work with non-zero values, handling zero values
|
||||
// gracefully through the Option type.
|
||||
//
|
||||
// The prism's GetOption returns Some(t) if the value is not equal to the zero value
|
||||
// of type T; otherwise, it returns None.
|
||||
//
|
||||
// The prism's ReverseGet is the identity function, returning the value unchanged.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: A comparable type (must support == and != operators)
|
||||
//
|
||||
// Returns:
|
||||
// - A Prism[T, T] that matches non-zero values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a prism for non-zero integers
|
||||
// nonZeroPrism := FromNonZero[int]()
|
||||
//
|
||||
// // Match non-zero value
|
||||
// result := nonZeroPrism.GetOption(42) // Some(42)
|
||||
//
|
||||
// // Zero returns None
|
||||
// result = nonZeroPrism.GetOption(0) // None[int]()
|
||||
//
|
||||
// // ReverseGet is identity
|
||||
// value := nonZeroPrism.ReverseGet(42) // 42
|
||||
//
|
||||
// // Use with Set to update non-zero values
|
||||
// setter := Set[int, int](100)
|
||||
// result := setter(nonZeroPrism)(42) // 100
|
||||
// result = setter(nonZeroPrism)(0) // 0 (unchanged)
|
||||
//
|
||||
// Common use cases:
|
||||
// - Validating that values are non-zero/non-default
|
||||
// - Filtering non-zero values in data pipelines
|
||||
// - Working with required fields that shouldn't be zero
|
||||
// - Replacing non-zero values with new values
|
||||
func FromNonZero[T comparable]() Prism[T, T] {
|
||||
return MakePrism(option.FromNonZero[T](), F.Identity[T])
|
||||
return MakePrismWithName(option.FromNonZero[T](), F.Identity[T], "PrismFromNonZero")
|
||||
}
|
||||
|
||||
// Match represents a regex match result with full reconstruction capability.
|
||||
@@ -495,7 +543,7 @@ func (m Match) Group(n int) string {
|
||||
func RegexMatcher(re *regexp.Regexp) Prism[string, Match] {
|
||||
noMatch := option.None[Match]()
|
||||
|
||||
return MakePrism(
|
||||
return MakePrismWithName(
|
||||
// String -> Option[Match]
|
||||
func(s string) Option[Match] {
|
||||
loc := re.FindStringSubmatchIndex(s)
|
||||
@@ -522,6 +570,7 @@ func RegexMatcher(re *regexp.Regexp) Prism[string, Match] {
|
||||
return option.Some(match)
|
||||
},
|
||||
Match.Reconstruct,
|
||||
fmt.Sprintf("PrismRegex[%s]", re),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -660,3 +709,259 @@ func RegexNamedMatcher(re *regexp.Regexp) Prism[string, NamedMatch] {
|
||||
NamedMatch.Reconstruct,
|
||||
)
|
||||
}
|
||||
|
||||
func getFromEither[A, B any](f func(A) (B, error)) func(A) Option[B] {
|
||||
return func(a A) Option[B] {
|
||||
b, err := f(a)
|
||||
if err != nil {
|
||||
return option.None[B]()
|
||||
}
|
||||
return option.Of(b)
|
||||
}
|
||||
}
|
||||
|
||||
func atoi64(s string) (int64, error) {
|
||||
return strconv.ParseInt(s, 10, 64)
|
||||
}
|
||||
|
||||
func itoa64(i int64) string {
|
||||
return strconv.FormatInt(i, 10)
|
||||
}
|
||||
|
||||
// ParseInt creates a prism for parsing and formatting integers.
|
||||
// It provides a safe way to convert between string and int, handling
|
||||
// parsing errors gracefully through the Option type.
|
||||
//
|
||||
// The prism's GetOption attempts to parse a string into an int.
|
||||
// If parsing succeeds, it returns Some(int); if it fails (e.g., invalid
|
||||
// number format), it returns None.
|
||||
//
|
||||
// The prism's ReverseGet always succeeds, converting an int to its string representation.
|
||||
//
|
||||
// Returns:
|
||||
// - A Prism[string, int] that safely handles int parsing/formatting
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create an int parsing prism
|
||||
// intPrism := ParseInt()
|
||||
//
|
||||
// // Parse valid integer
|
||||
// parsed := intPrism.GetOption("42") // Some(42)
|
||||
//
|
||||
// // Parse invalid integer
|
||||
// invalid := intPrism.GetOption("not-a-number") // None[int]()
|
||||
//
|
||||
// // Format int to string
|
||||
// str := intPrism.ReverseGet(42) // "42"
|
||||
//
|
||||
// // Use with Set to update integer values
|
||||
// setter := Set[string, int](100)
|
||||
// result := setter(intPrism)("42") // "100"
|
||||
//
|
||||
// Common use cases:
|
||||
// - Parsing integer configuration values
|
||||
// - Validating numeric user input
|
||||
// - Converting between string and int in data pipelines
|
||||
// - Working with numeric API parameters
|
||||
//
|
||||
//go:inline
|
||||
func ParseInt() Prism[string, int] {
|
||||
return MakePrismWithName(getFromEither(strconv.Atoi), strconv.Itoa, "PrismParseInt")
|
||||
}
|
||||
|
||||
// ParseInt64 creates a prism for parsing and formatting 64-bit integers.
|
||||
// It provides a safe way to convert between string and int64, handling
|
||||
// parsing errors gracefully through the Option type.
|
||||
//
|
||||
// The prism's GetOption attempts to parse a string into an int64.
|
||||
// If parsing succeeds, it returns Some(int64); if it fails (e.g., invalid
|
||||
// number format or overflow), it returns None.
|
||||
//
|
||||
// The prism's ReverseGet always succeeds, converting an int64 to its string representation.
|
||||
//
|
||||
// Returns:
|
||||
// - A Prism[string, int64] that safely handles int64 parsing/formatting
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create an int64 parsing prism
|
||||
// int64Prism := ParseInt64()
|
||||
//
|
||||
// // Parse valid 64-bit integer
|
||||
// parsed := int64Prism.GetOption("9223372036854775807") // Some(9223372036854775807)
|
||||
//
|
||||
// // Parse invalid integer
|
||||
// invalid := int64Prism.GetOption("not-a-number") // None[int64]()
|
||||
//
|
||||
// // Format int64 to string
|
||||
// str := int64Prism.ReverseGet(int64(42)) // "42"
|
||||
//
|
||||
// // Use with Set to update int64 values
|
||||
// setter := Set[string, int64](int64(100))
|
||||
// result := setter(int64Prism)("42") // "100"
|
||||
//
|
||||
// Common use cases:
|
||||
// - Parsing large integer values (timestamps, IDs)
|
||||
// - Working with database integer columns
|
||||
// - Handling 64-bit numeric API parameters
|
||||
// - Converting between string and int64 in data pipelines
|
||||
//
|
||||
//go:inline
|
||||
func ParseInt64() Prism[string, int64] {
|
||||
return MakePrismWithName(getFromEither(atoi64), itoa64, "PrismParseInt64")
|
||||
}
|
||||
|
||||
// ParseBool creates a prism for parsing and formatting boolean values.
|
||||
// It provides a safe way to convert between string and bool, handling
|
||||
// parsing errors gracefully through the Option type.
|
||||
//
|
||||
// The prism's GetOption attempts to parse a string into a bool.
|
||||
// It accepts "1", "t", "T", "TRUE", "true", "True", "0", "f", "F", "FALSE", "false", "False".
|
||||
// If parsing succeeds, it returns Some(bool); if it fails, it returns None.
|
||||
//
|
||||
// The prism's ReverseGet always succeeds, converting a bool to "true" or "false".
|
||||
//
|
||||
// Returns:
|
||||
// - A Prism[string, bool] that safely handles bool parsing/formatting
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a bool parsing prism
|
||||
// boolPrism := ParseBool()
|
||||
//
|
||||
// // Parse valid boolean strings
|
||||
// parsed := boolPrism.GetOption("true") // Some(true)
|
||||
// parsed = boolPrism.GetOption("1") // Some(true)
|
||||
// parsed = boolPrism.GetOption("false") // Some(false)
|
||||
// parsed = boolPrism.GetOption("0") // Some(false)
|
||||
//
|
||||
// // Parse invalid boolean
|
||||
// invalid := boolPrism.GetOption("maybe") // None[bool]()
|
||||
//
|
||||
// // Format bool to string
|
||||
// str := boolPrism.ReverseGet(true) // "true"
|
||||
// str = boolPrism.ReverseGet(false) // "false"
|
||||
//
|
||||
// // Use with Set to update boolean values
|
||||
// setter := Set[string, bool](true)
|
||||
// result := setter(boolPrism)("false") // "true"
|
||||
//
|
||||
// Common use cases:
|
||||
// - Parsing boolean configuration values
|
||||
// - Validating boolean user input
|
||||
// - Converting between string and bool in data pipelines
|
||||
// - Working with boolean API parameters or flags
|
||||
//
|
||||
//go:inline
|
||||
func ParseBool() Prism[string, bool] {
|
||||
return MakePrismWithName(getFromEither(strconv.ParseBool), strconv.FormatBool, "PrismParseBool")
|
||||
}
|
||||
|
||||
func atof64(s string) (float64, error) {
|
||||
return strconv.ParseFloat(s, 64)
|
||||
}
|
||||
|
||||
func atof32(s string) (float32, error) {
|
||||
f32, err := strconv.ParseFloat(s, 32)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return float32(f32), nil
|
||||
}
|
||||
|
||||
func f32toa(f float32) string {
|
||||
return strconv.FormatFloat(float64(f), 'g', -1, 32)
|
||||
}
|
||||
|
||||
func f64toa(f float64) string {
|
||||
return strconv.FormatFloat(f, 'g', -1, 64)
|
||||
}
|
||||
|
||||
// ParseFloat32 creates a prism for parsing and formatting 32-bit floating-point numbers.
|
||||
// It provides a safe way to convert between string and float32, handling
|
||||
// parsing errors gracefully through the Option type.
|
||||
//
|
||||
// The prism's GetOption attempts to parse a string into a float32.
|
||||
// If parsing succeeds, it returns Some(float32); if it fails (e.g., invalid
|
||||
// number format or overflow), it returns None.
|
||||
//
|
||||
// The prism's ReverseGet always succeeds, converting a float32 to its string representation
|
||||
// using the 'g' format (shortest representation).
|
||||
//
|
||||
// Returns:
|
||||
// - A Prism[string, float32] that safely handles float32 parsing/formatting
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a float32 parsing prism
|
||||
// float32Prism := ParseFloat32()
|
||||
//
|
||||
// // Parse valid float
|
||||
// parsed := float32Prism.GetOption("3.14") // Some(3.14)
|
||||
// parsed = float32Prism.GetOption("1.5e10") // Some(1.5e10)
|
||||
//
|
||||
// // Parse invalid float
|
||||
// invalid := float32Prism.GetOption("not-a-number") // None[float32]()
|
||||
//
|
||||
// // Format float32 to string
|
||||
// str := float32Prism.ReverseGet(float32(3.14)) // "3.14"
|
||||
//
|
||||
// // Use with Set to update float32 values
|
||||
// setter := Set[string, float32](float32(2.71))
|
||||
// result := setter(float32Prism)("3.14") // "2.71"
|
||||
//
|
||||
// Common use cases:
|
||||
// - Parsing floating-point configuration values
|
||||
// - Working with scientific notation
|
||||
// - Converting between string and float32 in data pipelines
|
||||
// - Handling numeric API parameters with decimal precision
|
||||
//
|
||||
//go:inline
|
||||
func ParseFloat32() Prism[string, float32] {
|
||||
return MakePrismWithName(getFromEither(atof32), f32toa, "ParseFloat32")
|
||||
}
|
||||
|
||||
// ParseFloat64 creates a prism for parsing and formatting 64-bit floating-point numbers.
|
||||
// It provides a safe way to convert between string and float64, handling
|
||||
// parsing errors gracefully through the Option type.
|
||||
//
|
||||
// The prism's GetOption attempts to parse a string into a float64.
|
||||
// If parsing succeeds, it returns Some(float64); if it fails (e.g., invalid
|
||||
// number format or overflow), it returns None.
|
||||
//
|
||||
// The prism's ReverseGet always succeeds, converting a float64 to its string representation
|
||||
// using the 'g' format (shortest representation).
|
||||
//
|
||||
// Returns:
|
||||
// - A Prism[string, float64] that safely handles float64 parsing/formatting
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a float64 parsing prism
|
||||
// float64Prism := ParseFloat64()
|
||||
//
|
||||
// // Parse valid float
|
||||
// parsed := float64Prism.GetOption("3.141592653589793") // Some(3.141592653589793)
|
||||
// parsed = float64Prism.GetOption("1.5e100") // Some(1.5e100)
|
||||
//
|
||||
// // Parse invalid float
|
||||
// invalid := float64Prism.GetOption("not-a-number") // None[float64]()
|
||||
//
|
||||
// // Format float64 to string
|
||||
// str := float64Prism.ReverseGet(3.141592653589793) // "3.141592653589793"
|
||||
//
|
||||
// // Use with Set to update float64 values
|
||||
// setter := Set[string, float64](2.718281828459045)
|
||||
// result := setter(float64Prism)("3.14") // "2.718281828459045"
|
||||
//
|
||||
// Common use cases:
|
||||
// - Parsing high-precision floating-point values
|
||||
// - Working with scientific notation and large numbers
|
||||
// - Converting between string and float64 in data pipelines
|
||||
// - Handling precise numeric API parameters
|
||||
//
|
||||
//go:inline
|
||||
func ParseFloat64() Prism[string, float64] {
|
||||
return MakePrismWithName(getFromEither(atof64), f64toa, "PrismParseFloat64")
|
||||
}
|
||||
|
||||
@@ -532,3 +532,411 @@ func TestRegexNamedMatcherWithSet(t *testing.T) {
|
||||
assert.Equal(t, original, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromNonZero tests the FromNonZero prism with various comparable types
|
||||
func TestFromNonZero(t *testing.T) {
|
||||
t.Run("int - match non-zero", func(t *testing.T) {
|
||||
prism := FromNonZero[int]()
|
||||
|
||||
result := prism.GetOption(42)
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, 42, O.GetOrElse(F.Constant(-1))(result))
|
||||
})
|
||||
|
||||
t.Run("int - zero returns None", func(t *testing.T) {
|
||||
prism := FromNonZero[int]()
|
||||
|
||||
result := prism.GetOption(0)
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("string - match non-empty string", func(t *testing.T) {
|
||||
prism := FromNonZero[string]()
|
||||
|
||||
result := prism.GetOption("hello")
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, "hello", O.GetOrElse(F.Constant("default"))(result))
|
||||
})
|
||||
|
||||
t.Run("string - empty returns None", func(t *testing.T) {
|
||||
prism := FromNonZero[string]()
|
||||
|
||||
result := prism.GetOption("")
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("bool - match true", func(t *testing.T) {
|
||||
prism := FromNonZero[bool]()
|
||||
|
||||
result := prism.GetOption(true)
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.True(t, O.GetOrElse(F.Constant(false))(result))
|
||||
})
|
||||
|
||||
t.Run("bool - false returns None", func(t *testing.T) {
|
||||
prism := FromNonZero[bool]()
|
||||
|
||||
result := prism.GetOption(false)
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("float64 - match non-zero", func(t *testing.T) {
|
||||
prism := FromNonZero[float64]()
|
||||
|
||||
result := prism.GetOption(3.14)
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, 3.14, O.GetOrElse(F.Constant(-1.0))(result))
|
||||
})
|
||||
|
||||
t.Run("float64 - zero returns None", func(t *testing.T) {
|
||||
prism := FromNonZero[float64]()
|
||||
|
||||
result := prism.GetOption(0.0)
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("pointer - match non-nil", func(t *testing.T) {
|
||||
prism := FromNonZero[*int]()
|
||||
|
||||
value := 42
|
||||
result := prism.GetOption(&value)
|
||||
assert.True(t, O.IsSome(result))
|
||||
})
|
||||
|
||||
t.Run("pointer - nil returns None", func(t *testing.T) {
|
||||
prism := FromNonZero[*int]()
|
||||
|
||||
var nilPtr *int
|
||||
result := prism.GetOption(nilPtr)
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("reverse get is identity", func(t *testing.T) {
|
||||
prism := FromNonZero[int]()
|
||||
|
||||
assert.Equal(t, 0, prism.ReverseGet(0))
|
||||
assert.Equal(t, 42, prism.ReverseGet(42))
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromNonZeroWithSet tests using Set with FromNonZero prism
|
||||
func TestFromNonZeroWithSet(t *testing.T) {
|
||||
t.Run("set on non-zero value", func(t *testing.T) {
|
||||
prism := FromNonZero[int]()
|
||||
|
||||
setter := Set[int](100)
|
||||
result := setter(prism)(42)
|
||||
|
||||
assert.Equal(t, 100, result)
|
||||
})
|
||||
|
||||
t.Run("set on zero returns original", func(t *testing.T) {
|
||||
prism := FromNonZero[int]()
|
||||
|
||||
setter := Set[int](100)
|
||||
result := setter(prism)(0)
|
||||
|
||||
assert.Equal(t, 0, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestParseInt tests the ParseInt prism
|
||||
func TestParseInt(t *testing.T) {
|
||||
prism := ParseInt()
|
||||
|
||||
t.Run("parse valid positive integer", func(t *testing.T) {
|
||||
result := prism.GetOption("42")
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, 42, O.GetOrElse(F.Constant(-1))(result))
|
||||
})
|
||||
|
||||
t.Run("parse valid negative integer", func(t *testing.T) {
|
||||
result := prism.GetOption("-123")
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, -123, O.GetOrElse(F.Constant(0))(result))
|
||||
})
|
||||
|
||||
t.Run("parse zero", func(t *testing.T) {
|
||||
result := prism.GetOption("0")
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, 0, O.GetOrElse(F.Constant(-1))(result))
|
||||
})
|
||||
|
||||
t.Run("parse invalid integer", func(t *testing.T) {
|
||||
result := prism.GetOption("not-a-number")
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("parse float as integer fails", func(t *testing.T) {
|
||||
result := prism.GetOption("3.14")
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("parse empty string fails", func(t *testing.T) {
|
||||
result := prism.GetOption("")
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("reverse get formats integer", func(t *testing.T) {
|
||||
assert.Equal(t, "42", prism.ReverseGet(42))
|
||||
assert.Equal(t, "-123", prism.ReverseGet(-123))
|
||||
assert.Equal(t, "0", prism.ReverseGet(0))
|
||||
})
|
||||
|
||||
t.Run("round trip", func(t *testing.T) {
|
||||
original := "12345"
|
||||
result := prism.GetOption(original)
|
||||
if O.IsSome(result) {
|
||||
value := O.GetOrElse(F.Constant(0))(result)
|
||||
reconstructed := prism.ReverseGet(value)
|
||||
assert.Equal(t, original, reconstructed)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestParseInt64 tests the ParseInt64 prism
|
||||
func TestParseInt64(t *testing.T) {
|
||||
prism := ParseInt64()
|
||||
|
||||
t.Run("parse valid int64", func(t *testing.T) {
|
||||
result := prism.GetOption("9223372036854775807")
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, int64(9223372036854775807), O.GetOrElse(F.Constant(int64(-1)))(result))
|
||||
})
|
||||
|
||||
t.Run("parse negative int64", func(t *testing.T) {
|
||||
result := prism.GetOption("-9223372036854775808")
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, int64(-9223372036854775808), O.GetOrElse(F.Constant(int64(0)))(result))
|
||||
})
|
||||
|
||||
t.Run("parse invalid int64", func(t *testing.T) {
|
||||
result := prism.GetOption("not-a-number")
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("reverse get formats int64", func(t *testing.T) {
|
||||
assert.Equal(t, "42", prism.ReverseGet(int64(42)))
|
||||
assert.Equal(t, "9223372036854775807", prism.ReverseGet(int64(9223372036854775807)))
|
||||
})
|
||||
|
||||
t.Run("round trip", func(t *testing.T) {
|
||||
original := "1234567890123456789"
|
||||
result := prism.GetOption(original)
|
||||
if O.IsSome(result) {
|
||||
value := O.GetOrElse(F.Constant(int64(0)))(result)
|
||||
reconstructed := prism.ReverseGet(value)
|
||||
assert.Equal(t, original, reconstructed)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestParseBool tests the ParseBool prism
|
||||
func TestParseBool(t *testing.T) {
|
||||
prism := ParseBool()
|
||||
|
||||
t.Run("parse true variations", func(t *testing.T) {
|
||||
trueValues := []string{"true", "True", "TRUE", "t", "T", "1"}
|
||||
for _, val := range trueValues {
|
||||
result := prism.GetOption(val)
|
||||
assert.True(t, O.IsSome(result), "Should parse: %s", val)
|
||||
assert.True(t, O.GetOrElse(F.Constant(false))(result), "Should be true: %s", val)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("parse false variations", func(t *testing.T) {
|
||||
falseValues := []string{"false", "False", "FALSE", "f", "F", "0"}
|
||||
for _, val := range falseValues {
|
||||
result := prism.GetOption(val)
|
||||
assert.True(t, O.IsSome(result), "Should parse: %s", val)
|
||||
assert.False(t, O.GetOrElse(F.Constant(true))(result), "Should be false: %s", val)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("parse invalid bool", func(t *testing.T) {
|
||||
invalidValues := []string{"maybe", "yes", "no", "2", ""}
|
||||
for _, val := range invalidValues {
|
||||
result := prism.GetOption(val)
|
||||
assert.True(t, O.IsNone(result), "Should not parse: %s", val)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("reverse get formats bool", func(t *testing.T) {
|
||||
assert.Equal(t, "true", prism.ReverseGet(true))
|
||||
assert.Equal(t, "false", prism.ReverseGet(false))
|
||||
})
|
||||
|
||||
t.Run("round trip with true", func(t *testing.T) {
|
||||
result := prism.GetOption("true")
|
||||
if O.IsSome(result) {
|
||||
value := O.GetOrElse(F.Constant(false))(result)
|
||||
reconstructed := prism.ReverseGet(value)
|
||||
assert.Equal(t, "true", reconstructed)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("round trip with false", func(t *testing.T) {
|
||||
result := prism.GetOption("false")
|
||||
if O.IsSome(result) {
|
||||
value := O.GetOrElse(F.Constant(true))(result)
|
||||
reconstructed := prism.ReverseGet(value)
|
||||
assert.Equal(t, "false", reconstructed)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestParseFloat32 tests the ParseFloat32 prism
|
||||
func TestParseFloat32(t *testing.T) {
|
||||
prism := ParseFloat32()
|
||||
|
||||
t.Run("parse valid float32", func(t *testing.T) {
|
||||
result := prism.GetOption("3.14")
|
||||
assert.True(t, O.IsSome(result))
|
||||
value := O.GetOrElse(F.Constant(float32(0)))(result)
|
||||
assert.InDelta(t, float32(3.14), value, 0.0001)
|
||||
})
|
||||
|
||||
t.Run("parse negative float32", func(t *testing.T) {
|
||||
result := prism.GetOption("-2.71")
|
||||
assert.True(t, O.IsSome(result))
|
||||
value := O.GetOrElse(F.Constant(float32(0)))(result)
|
||||
assert.InDelta(t, float32(-2.71), value, 0.0001)
|
||||
})
|
||||
|
||||
t.Run("parse scientific notation", func(t *testing.T) {
|
||||
result := prism.GetOption("1.5e10")
|
||||
assert.True(t, O.IsSome(result))
|
||||
value := O.GetOrElse(F.Constant(float32(0)))(result)
|
||||
assert.InDelta(t, float32(1.5e10), value, 1e6)
|
||||
})
|
||||
|
||||
t.Run("parse integer as float", func(t *testing.T) {
|
||||
result := prism.GetOption("42")
|
||||
assert.True(t, O.IsSome(result))
|
||||
value := O.GetOrElse(F.Constant(float32(0)))(result)
|
||||
assert.Equal(t, float32(42), value)
|
||||
})
|
||||
|
||||
t.Run("parse invalid float", func(t *testing.T) {
|
||||
result := prism.GetOption("not-a-number")
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("reverse get formats float32", func(t *testing.T) {
|
||||
str := prism.ReverseGet(float32(3.14))
|
||||
assert.Contains(t, str, "3.14")
|
||||
})
|
||||
|
||||
t.Run("round trip", func(t *testing.T) {
|
||||
original := "3.14159"
|
||||
result := prism.GetOption(original)
|
||||
if O.IsSome(result) {
|
||||
value := O.GetOrElse(F.Constant(float32(0)))(result)
|
||||
reconstructed := prism.ReverseGet(value)
|
||||
// Parse both to compare as floats due to precision
|
||||
origFloat := F.Pipe1(original, prism.GetOption)
|
||||
reconFloat := F.Pipe1(reconstructed, prism.GetOption)
|
||||
if O.IsSome(origFloat) && O.IsSome(reconFloat) {
|
||||
assert.InDelta(t,
|
||||
O.GetOrElse(F.Constant(float32(0)))(origFloat),
|
||||
O.GetOrElse(F.Constant(float32(0)))(reconFloat),
|
||||
0.0001)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestParseFloat64 tests the ParseFloat64 prism
|
||||
func TestParseFloat64(t *testing.T) {
|
||||
prism := ParseFloat64()
|
||||
|
||||
t.Run("parse valid float64", func(t *testing.T) {
|
||||
result := prism.GetOption("3.141592653589793")
|
||||
assert.True(t, O.IsSome(result))
|
||||
value := O.GetOrElse(F.Constant(0.0))(result)
|
||||
assert.InDelta(t, 3.141592653589793, value, 1e-15)
|
||||
})
|
||||
|
||||
t.Run("parse negative float64", func(t *testing.T) {
|
||||
result := prism.GetOption("-2.718281828459045")
|
||||
assert.True(t, O.IsSome(result))
|
||||
value := O.GetOrElse(F.Constant(0.0))(result)
|
||||
assert.InDelta(t, -2.718281828459045, value, 1e-15)
|
||||
})
|
||||
|
||||
t.Run("parse scientific notation", func(t *testing.T) {
|
||||
result := prism.GetOption("1.5e100")
|
||||
assert.True(t, O.IsSome(result))
|
||||
value := O.GetOrElse(F.Constant(0.0))(result)
|
||||
assert.InDelta(t, 1.5e100, value, 1e85)
|
||||
})
|
||||
|
||||
t.Run("parse integer as float", func(t *testing.T) {
|
||||
result := prism.GetOption("42")
|
||||
assert.True(t, O.IsSome(result))
|
||||
value := O.GetOrElse(F.Constant(0.0))(result)
|
||||
assert.Equal(t, 42.0, value)
|
||||
})
|
||||
|
||||
t.Run("parse invalid float", func(t *testing.T) {
|
||||
result := prism.GetOption("not-a-number")
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("reverse get formats float64", func(t *testing.T) {
|
||||
str := prism.ReverseGet(3.141592653589793)
|
||||
assert.Contains(t, str, "3.14159")
|
||||
})
|
||||
|
||||
t.Run("round trip", func(t *testing.T) {
|
||||
original := "3.141592653589793"
|
||||
result := prism.GetOption(original)
|
||||
if O.IsSome(result) {
|
||||
value := O.GetOrElse(F.Constant(0.0))(result)
|
||||
reconstructed := prism.ReverseGet(value)
|
||||
// Parse both to compare as floats
|
||||
origFloat := F.Pipe1(original, prism.GetOption)
|
||||
reconFloat := F.Pipe1(reconstructed, prism.GetOption)
|
||||
if O.IsSome(origFloat) && O.IsSome(reconFloat) {
|
||||
assert.InDelta(t,
|
||||
O.GetOrElse(F.Constant(0.0))(origFloat),
|
||||
O.GetOrElse(F.Constant(0.0))(reconFloat),
|
||||
1e-15)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestParseIntWithSet tests using Set with ParseInt prism
|
||||
func TestParseIntWithSet(t *testing.T) {
|
||||
prism := ParseInt()
|
||||
|
||||
t.Run("set on valid integer string", func(t *testing.T) {
|
||||
setter := Set[string](100)
|
||||
result := setter(prism)("42")
|
||||
assert.Equal(t, "100", result)
|
||||
})
|
||||
|
||||
t.Run("set on invalid string returns original", func(t *testing.T) {
|
||||
setter := Set[string](100)
|
||||
result := setter(prism)("not-a-number")
|
||||
assert.Equal(t, "not-a-number", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestParseBoolWithSet tests using Set with ParseBool prism
|
||||
func TestParseBoolWithSet(t *testing.T) {
|
||||
prism := ParseBool()
|
||||
|
||||
t.Run("set on valid bool string", func(t *testing.T) {
|
||||
setter := Set[string](true)
|
||||
result := setter(prism)("false")
|
||||
assert.Equal(t, "true", result)
|
||||
})
|
||||
|
||||
t.Run("set on invalid string returns original", func(t *testing.T) {
|
||||
setter := Set[string](true)
|
||||
result := setter(prism)("maybe")
|
||||
assert.Equal(t, "maybe", result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ package option
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
// Benchmark basic construction
|
||||
@@ -46,7 +48,7 @@ func BenchmarkIsSome(b *testing.B) {
|
||||
|
||||
func BenchmarkMap(b *testing.B) {
|
||||
opt := Some(21)
|
||||
mapper := Map(func(x int) int { return x * 2 })
|
||||
mapper := Map(N.Mul(2))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
||||
@@ -17,6 +17,8 @@ package option
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
// Benchmark shallow chain (1 step)
|
||||
@@ -121,11 +123,11 @@ func BenchmarkMap_5Steps(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = Map(func(x int) int { return x - 10 })(
|
||||
Map(func(x int) int { return x / 2 })(
|
||||
Map(func(x int) int { return x + 20 })(
|
||||
Map(func(x int) int { return x * 3 })(
|
||||
Map(func(x int) int { return x + 1 })(opt),
|
||||
_ = Map(N.Sub(10))(
|
||||
Map(N.Div(2))(
|
||||
Map(N.Add(20))(
|
||||
Map(N.Mul(3))(
|
||||
Map(N.Add(1))(opt),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -17,6 +17,8 @@ package reader
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
RA "github.com/IBM/fp-go/v2/internal/array"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
G "github.com/IBM/fp-go/v2/reader/generic"
|
||||
)
|
||||
|
||||
@@ -100,3 +102,273 @@ func TraverseArrayWithIndex[R, A, B any](f func(int, A) Reader[R, B]) func([]A)
|
||||
func SequenceArray[R, A any](ma []Reader[R, A]) Reader[R, []A] {
|
||||
return MonadTraverseArray(ma, function.Identity[Reader[R, A]])
|
||||
}
|
||||
|
||||
// MonadReduceArray reduces an array of Readers to a single Reader by applying a reduction function.
|
||||
// This is the monadic version that takes the array of Readers as the first parameter.
|
||||
//
|
||||
// Each Reader is evaluated with the same environment R, and the results are accumulated using
|
||||
// the provided reduce function starting from the initial value.
|
||||
//
|
||||
// Parameters:
|
||||
// - as: Array of Readers to reduce
|
||||
// - reduce: Binary function that combines accumulated value with each Reader's result
|
||||
// - initial: Starting value for the reduction
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Base int }
|
||||
// readers := []reader.Reader[Config, int]{
|
||||
// reader.Asks(func(c Config) int { return c.Base + 1 }),
|
||||
// reader.Asks(func(c Config) int { return c.Base + 2 }),
|
||||
// reader.Asks(func(c Config) int { return c.Base + 3 }),
|
||||
// }
|
||||
// sum := func(acc, val int) int { return acc + val }
|
||||
// r := reader.MonadReduceArray(readers, sum, 0)
|
||||
// result := r(Config{Base: 10}) // 36 (11 + 12 + 13)
|
||||
//
|
||||
//go:inline
|
||||
func MonadReduceArray[R, A, B any](as []Reader[R, A], reduce func(B, A) B, initial B) Reader[R, B] {
|
||||
return RA.MonadTraverseReduce(
|
||||
Of,
|
||||
Map,
|
||||
Ap,
|
||||
|
||||
as,
|
||||
|
||||
function.Identity[Reader[R, A]],
|
||||
reduce,
|
||||
initial,
|
||||
)
|
||||
}
|
||||
|
||||
// ReduceArray returns a curried function that reduces an array of Readers to a single Reader.
|
||||
// This is the curried version where the reduction function and initial value are provided first,
|
||||
// returning a function that takes the array of Readers.
|
||||
//
|
||||
// Parameters:
|
||||
// - reduce: Binary function that combines accumulated value with each Reader's result
|
||||
// - initial: Starting value for the reduction
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an array of Readers and returns a Reader of the reduced result
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Multiplier int }
|
||||
// product := func(acc, val int) int { return acc * val }
|
||||
// reducer := reader.ReduceArray[Config](product, 1)
|
||||
// readers := []reader.Reader[Config, int]{
|
||||
// reader.Asks(func(c Config) int { return c.Multiplier * 2 }),
|
||||
// reader.Asks(func(c Config) int { return c.Multiplier * 3 }),
|
||||
// }
|
||||
// r := reducer(readers)
|
||||
// result := r(Config{Multiplier: 5}) // 150 (10 * 15)
|
||||
//
|
||||
//go:inline
|
||||
func ReduceArray[R, A, B any](reduce func(B, A) B, initial B) Kleisli[R, []Reader[R, A], B] {
|
||||
return RA.TraverseReduce[[]Reader[R, A]](
|
||||
Of,
|
||||
Map,
|
||||
Ap,
|
||||
|
||||
function.Identity[Reader[R, A]],
|
||||
reduce,
|
||||
initial,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadReduceArrayM reduces an array of Readers using a Monoid to combine the results.
|
||||
// This is the monadic version that takes the array of Readers as the first parameter.
|
||||
//
|
||||
// The Monoid provides both the binary operation (Concat) and the identity element (Empty)
|
||||
// for the reduction, making it convenient when working with monoidal types.
|
||||
//
|
||||
// Parameters:
|
||||
// - as: Array of Readers to reduce
|
||||
// - m: Monoid that defines how to combine the Reader results
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Factor int }
|
||||
// readers := []reader.Reader[Config, int]{
|
||||
// reader.Asks(func(c Config) int { return c.Factor }),
|
||||
// reader.Asks(func(c Config) int { return c.Factor * 2 }),
|
||||
// reader.Asks(func(c Config) int { return c.Factor * 3 }),
|
||||
// }
|
||||
// intAddMonoid := monoid.MakeMonoid(func(a, b int) int { return a + b }, 0)
|
||||
// r := reader.MonadReduceArrayM(readers, intAddMonoid)
|
||||
// result := r(Config{Factor: 5}) // 30 (5 + 10 + 15)
|
||||
//
|
||||
//go:inline
|
||||
func MonadReduceArrayM[R, A any](as []Reader[R, A], m monoid.Monoid[A]) Reader[R, A] {
|
||||
return MonadReduceArray(as, m.Concat, m.Empty())
|
||||
}
|
||||
|
||||
// ReduceArrayM returns a curried function that reduces an array of Readers using a Monoid.
|
||||
// This is the curried version where the Monoid is provided first, returning a function
|
||||
// that takes the array of Readers.
|
||||
//
|
||||
// The Monoid provides both the binary operation (Concat) and the identity element (Empty)
|
||||
// for the reduction.
|
||||
//
|
||||
// Parameters:
|
||||
// - m: Monoid that defines how to combine the Reader results
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an array of Readers and returns a Reader of the reduced result
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Scale int }
|
||||
// intMultMonoid := monoid.MakeMonoid(func(a, b int) int { return a * b }, 1)
|
||||
// reducer := reader.ReduceArrayM[Config](intMultMonoid)
|
||||
// readers := []reader.Reader[Config, int]{
|
||||
// reader.Asks(func(c Config) int { return c.Scale }),
|
||||
// reader.Asks(func(c Config) int { return c.Scale * 2 }),
|
||||
// }
|
||||
// r := reducer(readers)
|
||||
// result := r(Config{Scale: 3}) // 18 (3 * 6)
|
||||
//
|
||||
//go:inline
|
||||
func ReduceArrayM[R, A any](m monoid.Monoid[A]) Kleisli[R, []Reader[R, A], A] {
|
||||
return ReduceArray[R](m.Concat, m.Empty())
|
||||
}
|
||||
|
||||
// MonadTraverseReduceArray transforms and reduces an array in one operation.
|
||||
// This is the monadic version that takes the array as the first parameter.
|
||||
//
|
||||
// First, each element is transformed using the provided Kleisli function into a Reader.
|
||||
// Then, the Reader results are reduced using the provided reduction function.
|
||||
//
|
||||
// This is more efficient than calling TraverseArray followed by a separate reduce operation,
|
||||
// as it combines both operations into a single traversal.
|
||||
//
|
||||
// Parameters:
|
||||
// - as: Array of elements to transform and reduce
|
||||
// - trfrm: Function that transforms each element into a Reader
|
||||
// - reduce: Binary function that combines accumulated value with each transformed result
|
||||
// - initial: Starting value for the reduction
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Multiplier int }
|
||||
// numbers := []int{1, 2, 3, 4}
|
||||
// multiply := func(n int) reader.Reader[Config, int] {
|
||||
// return reader.Asks(func(c Config) int { return n * c.Multiplier })
|
||||
// }
|
||||
// sum := func(acc, val int) int { return acc + val }
|
||||
// r := reader.MonadTraverseReduceArray(numbers, multiply, sum, 0)
|
||||
// result := r(Config{Multiplier: 10}) // 100 (10 + 20 + 30 + 40)
|
||||
//
|
||||
//go:inline
|
||||
func MonadTraverseReduceArray[R, A, B, C any](as []A, trfrm Kleisli[R, A, B], reduce func(C, B) C, initial C) Reader[R, C] {
|
||||
return RA.MonadTraverseReduce(
|
||||
Of,
|
||||
Map,
|
||||
Ap,
|
||||
|
||||
as,
|
||||
|
||||
trfrm,
|
||||
reduce,
|
||||
initial,
|
||||
)
|
||||
}
|
||||
|
||||
// TraverseReduceArray returns a curried function that transforms and reduces an array.
|
||||
// This is the curried version where the transformation function, reduce function, and initial value
|
||||
// are provided first, returning a function that takes the array.
|
||||
//
|
||||
// First, each element is transformed using the provided Kleisli function into a Reader.
|
||||
// Then, the Reader results are reduced using the provided reduction function.
|
||||
//
|
||||
// Parameters:
|
||||
// - trfrm: Function that transforms each element into a Reader
|
||||
// - reduce: Binary function that combines accumulated value with each transformed result
|
||||
// - initial: Starting value for the reduction
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an array and returns a Reader of the reduced result
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Base int }
|
||||
// addBase := func(n int) reader.Reader[Config, int] {
|
||||
// return reader.Asks(func(c Config) int { return n + c.Base })
|
||||
// }
|
||||
// product := func(acc, val int) int { return acc * val }
|
||||
// transformer := reader.TraverseReduceArray(addBase, product, 1)
|
||||
// r := transformer([]int{2, 3, 4})
|
||||
// result := r(Config{Base: 10}) // 2184 (12 * 13 * 14)
|
||||
//
|
||||
//go:inline
|
||||
func TraverseReduceArray[R, A, B, C any](trfrm Kleisli[R, A, B], reduce func(C, B) C, initial C) Kleisli[R, []A, C] {
|
||||
return RA.TraverseReduce[[]A](
|
||||
Of,
|
||||
Map,
|
||||
Ap,
|
||||
|
||||
trfrm,
|
||||
reduce,
|
||||
initial,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadTraverseReduceArrayM transforms and reduces an array using a Monoid.
|
||||
// This is the monadic version that takes the array as the first parameter.
|
||||
//
|
||||
// First, each element is transformed using the provided Kleisli function into a Reader.
|
||||
// Then, the Reader results are reduced using the Monoid's binary operation and identity element.
|
||||
//
|
||||
// This combines transformation and monoidal reduction in a single efficient operation.
|
||||
//
|
||||
// Parameters:
|
||||
// - as: Array of elements to transform and reduce
|
||||
// - trfrm: Function that transforms each element into a Reader
|
||||
// - m: Monoid that defines how to combine the transformed results
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Offset int }
|
||||
// numbers := []int{1, 2, 3}
|
||||
// addOffset := func(n int) reader.Reader[Config, int] {
|
||||
// return reader.Asks(func(c Config) int { return n + c.Offset })
|
||||
// }
|
||||
// intSumMonoid := monoid.MakeMonoid(func(a, b int) int { return a + b }, 0)
|
||||
// r := reader.MonadTraverseReduceArrayM(numbers, addOffset, intSumMonoid)
|
||||
// result := r(Config{Offset: 100}) // 306 (101 + 102 + 103)
|
||||
//
|
||||
//go:inline
|
||||
func MonadTraverseReduceArrayM[R, A, B any](as []A, trfrm Kleisli[R, A, B], m monoid.Monoid[B]) Reader[R, B] {
|
||||
return MonadTraverseReduceArray(as, trfrm, m.Concat, m.Empty())
|
||||
}
|
||||
|
||||
// TraverseReduceArrayM returns a curried function that transforms and reduces an array using a Monoid.
|
||||
// This is the curried version where the transformation function and Monoid are provided first,
|
||||
// returning a function that takes the array.
|
||||
//
|
||||
// First, each element is transformed using the provided Kleisli function into a Reader.
|
||||
// Then, the Reader results are reduced using the Monoid's binary operation and identity element.
|
||||
//
|
||||
// Parameters:
|
||||
// - trfrm: Function that transforms each element into a Reader
|
||||
// - m: Monoid that defines how to combine the transformed results
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an array and returns a Reader of the reduced result
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Factor int }
|
||||
// scale := func(n int) reader.Reader[Config, int] {
|
||||
// return reader.Asks(func(c Config) int { return n * c.Factor })
|
||||
// }
|
||||
// intProdMonoid := monoid.MakeMonoid(func(a, b int) int { return a * b }, 1)
|
||||
// transformer := reader.TraverseReduceArrayM(scale, intProdMonoid)
|
||||
// r := transformer([]int{2, 3, 4})
|
||||
// result := r(Config{Factor: 5}) // 3000 (10 * 15 * 20)
|
||||
//
|
||||
//go:inline
|
||||
func TraverseReduceArrayM[R, A, B any](trfrm Kleisli[R, A, B], m monoid.Monoid[B]) Kleisli[R, []A, B] {
|
||||
return TraverseReduceArray(trfrm, m.Concat, m.Empty())
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -93,3 +94,142 @@ func TestMonadTraverseArray(t *testing.T) {
|
||||
assert.Equal(t, 3, len(result))
|
||||
assert.Contains(t, result[0], "num")
|
||||
}
|
||||
|
||||
func TestMonadReduceArray(t *testing.T) {
|
||||
type Config struct{ Base int }
|
||||
config := Config{Base: 10}
|
||||
|
||||
readers := []Reader[Config, int]{
|
||||
Asks(func(c Config) int { return c.Base + 1 }),
|
||||
Asks(func(c Config) int { return c.Base + 2 }),
|
||||
Asks(func(c Config) int { return c.Base + 3 }),
|
||||
}
|
||||
|
||||
sum := func(acc, val int) int { return acc + val }
|
||||
r := MonadReduceArray(readers, sum, 0)
|
||||
result := r(config)
|
||||
|
||||
assert.Equal(t, 36, result) // 11 + 12 + 13
|
||||
}
|
||||
|
||||
func TestReduceArray(t *testing.T) {
|
||||
type Config struct{ Multiplier int }
|
||||
config := Config{Multiplier: 5}
|
||||
|
||||
product := func(acc, val int) int { return acc * val }
|
||||
reducer := ReduceArray[Config](product, 1)
|
||||
|
||||
readers := []Reader[Config, int]{
|
||||
Asks(func(c Config) int { return c.Multiplier * 2 }),
|
||||
Asks(func(c Config) int { return c.Multiplier * 3 }),
|
||||
}
|
||||
|
||||
r := reducer(readers)
|
||||
result := r(config)
|
||||
|
||||
assert.Equal(t, 150, result) // 10 * 15
|
||||
}
|
||||
|
||||
func TestMonadReduceArrayM(t *testing.T) {
|
||||
type Config struct{ Factor int }
|
||||
config := Config{Factor: 5}
|
||||
|
||||
readers := []Reader[Config, int]{
|
||||
Asks(func(c Config) int { return c.Factor }),
|
||||
Asks(func(c Config) int { return c.Factor * 2 }),
|
||||
Asks(func(c Config) int { return c.Factor * 3 }),
|
||||
}
|
||||
|
||||
intAddMonoid := M.MakeMonoid(func(a, b int) int { return a + b }, 0)
|
||||
|
||||
r := MonadReduceArrayM(readers, intAddMonoid)
|
||||
result := r(config)
|
||||
|
||||
assert.Equal(t, 30, result) // 5 + 10 + 15
|
||||
}
|
||||
|
||||
func TestReduceArrayM(t *testing.T) {
|
||||
type Config struct{ Scale int }
|
||||
config := Config{Scale: 3}
|
||||
|
||||
intMultMonoid := M.MakeMonoid(func(a, b int) int { return a * b }, 1)
|
||||
|
||||
reducer := ReduceArrayM[Config](intMultMonoid)
|
||||
|
||||
readers := []Reader[Config, int]{
|
||||
Asks(func(c Config) int { return c.Scale }),
|
||||
Asks(func(c Config) int { return c.Scale * 2 }),
|
||||
}
|
||||
|
||||
r := reducer(readers)
|
||||
result := r(config)
|
||||
|
||||
assert.Equal(t, 18, result) // 3 * 6
|
||||
}
|
||||
|
||||
func TestMonadTraverseReduceArray(t *testing.T) {
|
||||
type Config struct{ Multiplier int }
|
||||
config := Config{Multiplier: 10}
|
||||
|
||||
numbers := []int{1, 2, 3, 4}
|
||||
multiply := func(n int) Reader[Config, int] {
|
||||
return Asks(func(c Config) int { return n * c.Multiplier })
|
||||
}
|
||||
|
||||
sum := func(acc, val int) int { return acc + val }
|
||||
r := MonadTraverseReduceArray(numbers, multiply, sum, 0)
|
||||
result := r(config)
|
||||
|
||||
assert.Equal(t, 100, result) // 10 + 20 + 30 + 40
|
||||
}
|
||||
|
||||
func TestTraverseReduceArray(t *testing.T) {
|
||||
type Config struct{ Base int }
|
||||
config := Config{Base: 10}
|
||||
|
||||
addBase := func(n int) Reader[Config, int] {
|
||||
return Asks(func(c Config) int { return n + c.Base })
|
||||
}
|
||||
|
||||
product := func(acc, val int) int { return acc * val }
|
||||
transformer := TraverseReduceArray(addBase, product, 1)
|
||||
|
||||
r := transformer([]int{2, 3, 4})
|
||||
result := r(config)
|
||||
|
||||
assert.Equal(t, 2184, result) // 12 * 13 * 14
|
||||
}
|
||||
|
||||
func TestMonadTraverseReduceArrayM(t *testing.T) {
|
||||
type Config struct{ Offset int }
|
||||
config := Config{Offset: 100}
|
||||
|
||||
numbers := []int{1, 2, 3}
|
||||
addOffset := func(n int) Reader[Config, int] {
|
||||
return Asks(func(c Config) int { return n + c.Offset })
|
||||
}
|
||||
|
||||
intSumMonoid := M.MakeMonoid(func(a, b int) int { return a + b }, 0)
|
||||
|
||||
r := MonadTraverseReduceArrayM(numbers, addOffset, intSumMonoid)
|
||||
result := r(config)
|
||||
|
||||
assert.Equal(t, 306, result) // 101 + 102 + 103
|
||||
}
|
||||
|
||||
func TestTraverseReduceArrayM(t *testing.T) {
|
||||
type Config struct{ Factor int }
|
||||
config := Config{Factor: 5}
|
||||
|
||||
scale := func(n int) Reader[Config, int] {
|
||||
return Asks(func(c Config) int { return n * c.Factor })
|
||||
}
|
||||
|
||||
intProdMonoid := M.MakeMonoid(func(a, b int) int { return a * b }, 1)
|
||||
|
||||
transformer := TraverseReduceArrayM(scale, intProdMonoid)
|
||||
r := transformer([]int{2, 3, 4})
|
||||
result := r(config)
|
||||
|
||||
assert.Equal(t, 3000, result) // 10 * 15 * 20
|
||||
}
|
||||
|
||||
@@ -289,11 +289,11 @@ func Read[A, E any](e E) func(Reader[E, A]) A {
|
||||
//
|
||||
// type Config struct { Multiplier int }
|
||||
// getMultiplier := func(c Config) func(int) int {
|
||||
// return func(x int) int { return x * c.Multiplier }
|
||||
// return N.Mul(c.Multiplier)
|
||||
// }
|
||||
// r := reader.MonadFlap(getMultiplier, 5)
|
||||
// result := r(Config{Multiplier: 3}) // 15
|
||||
func MonadFlap[R, A, B any](fab Reader[R, func(A) B], a A) Reader[R, B] {
|
||||
func MonadFlap[R, B, A any](fab Reader[R, func(A) B], a A) Reader[R, B] {
|
||||
return functor.MonadFlap(MonadMap[R, func(A) B, B], fab, a)
|
||||
}
|
||||
|
||||
@@ -304,11 +304,11 @@ func MonadFlap[R, A, B any](fab Reader[R, func(A) B], a A) Reader[R, B] {
|
||||
//
|
||||
// type Config struct { Multiplier int }
|
||||
// getMultiplier := reader.Asks(func(c Config) func(int) int {
|
||||
// return func(x int) int { return x * c.Multiplier }
|
||||
// return N.Mul(c.Multiplier)
|
||||
// })
|
||||
// applyTo5 := reader.Flap[Config](5)
|
||||
// r := applyTo5(getMultiplier)
|
||||
// result := r(Config{Multiplier: 3}) // 15
|
||||
func Flap[R, A, B any](a A) Operator[R, func(A) B, B] {
|
||||
func Flap[R, B, A any](a A) Operator[R, func(A) B, B] {
|
||||
return functor.Flap(Map[R, func(A) B, B], a)
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ func TestRead(t *testing.T) {
|
||||
func TestMonadFlap(t *testing.T) {
|
||||
config := Config{Multiplier: 3}
|
||||
getMultiplier := func(c Config) func(int) int {
|
||||
return func(x int) int { return x * c.Multiplier }
|
||||
return N.Mul(c.Multiplier)
|
||||
}
|
||||
r := MonadFlap(getMultiplier, 5)
|
||||
result := r(config)
|
||||
@@ -194,9 +194,9 @@ func TestMonadFlap(t *testing.T) {
|
||||
func TestFlap(t *testing.T) {
|
||||
config := Config{Multiplier: 3}
|
||||
getMultiplier := Asks(func(c Config) func(int) int {
|
||||
return func(x int) int { return x * c.Multiplier }
|
||||
return N.Mul(c.Multiplier)
|
||||
})
|
||||
applyTo5 := Flap[Config, int, int](5)
|
||||
applyTo5 := Flap[Config, int](5)
|
||||
r := applyTo5(getMultiplier)
|
||||
result := r(config)
|
||||
assert.Equal(t, 15, result)
|
||||
|
||||
68
v2/reader/record.go
Normal file
68
v2/reader/record.go
Normal file
@@ -0,0 +1,68 @@
|
||||
// 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 reader
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
RR "github.com/IBM/fp-go/v2/internal/record"
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func MonadTraverseRecord[K comparable, R, A, B any](ma map[K]A, f Kleisli[R, A, B]) Reader[R, map[K]B] {
|
||||
return RR.MonadTraverse[map[K]A, map[K]B](
|
||||
Of,
|
||||
Map,
|
||||
Ap,
|
||||
ma,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TraverseRecord[K comparable, R, A, B any](f Kleisli[R, A, B]) func(map[K]A) Reader[R, map[K]B] {
|
||||
return RR.Traverse[map[K]A, map[K]B](
|
||||
Of,
|
||||
Map,
|
||||
Ap,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadTraverseRecordWithIndex[K comparable, R, A, B any](ma map[K]A, f func(K, A) Reader[R, B]) Reader[R, map[K]B] {
|
||||
return RR.MonadTraverseWithIndex[map[K]A, map[K]B](
|
||||
Of,
|
||||
Map,
|
||||
Ap,
|
||||
ma,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TraverseRecordWithIndex[K comparable, R, A, B any](f func(K, A) Reader[R, B]) func(map[K]A) Reader[R, map[K]B] {
|
||||
return RR.TraverseWithIndex[map[K]A, map[K]B](
|
||||
Of,
|
||||
Map,
|
||||
Ap,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func SequenceRecord[K comparable, R, A any](ma map[K]Reader[R, A]) Reader[R, map[K]A] {
|
||||
return MonadTraverseRecord(ma, function.Identity[Reader[R, A]])
|
||||
}
|
||||
@@ -66,6 +66,14 @@ func Chain[E, L, A, B any](f func(A) ReaderEither[E, L, B]) func(ReaderEither[E,
|
||||
return readert.Chain[ReaderEither[E, L, A]](ET.Chain[L, A, B], f)
|
||||
}
|
||||
|
||||
func MonadChainReaderK[E, L, A, B any](ma ReaderEither[E, L, A], f reader.Kleisli[E, A, B]) ReaderEither[E, L, B] {
|
||||
return MonadChain(ma, function.Flow2(f, FromReader[E, L, B]))
|
||||
}
|
||||
|
||||
func ChainReaderK[E, L, A, B any](f reader.Kleisli[E, A, B]) func(ReaderEither[E, L, A]) ReaderEither[E, L, B] {
|
||||
return Chain(function.Flow2(f, FromReader[E, L, B]))
|
||||
}
|
||||
|
||||
func Of[E, L, A any](a A) ReaderEither[E, L, A] {
|
||||
return readert.MonadOf[ReaderEither[E, L, A]](ET.Of[L, A], a)
|
||||
}
|
||||
|
||||
83
v2/readerioeither/array.go
Normal file
83
v2/readerioeither/array.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package readerioeither
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
RA "github.com/IBM/fp-go/v2/internal/array"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func MonadReduceArray[R, E, A, B any](as []ReaderIOEither[R, E, A], reduce func(B, A) B, initial B) ReaderIOEither[R, E, B] {
|
||||
return RA.MonadTraverseReduce(
|
||||
Of,
|
||||
Map,
|
||||
Ap,
|
||||
|
||||
as,
|
||||
|
||||
function.Identity[ReaderIOEither[R, E, A]],
|
||||
reduce,
|
||||
initial,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ReduceArray[R, E, A, B any](reduce func(B, A) B, initial B) Kleisli[R, E, []ReaderIOEither[R, E, A], B] {
|
||||
return RA.TraverseReduce[[]ReaderIOEither[R, E, A]](
|
||||
Of,
|
||||
Map,
|
||||
Ap,
|
||||
|
||||
function.Identity[ReaderIOEither[R, E, A]],
|
||||
reduce,
|
||||
initial,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadReduceArrayM[R, E, A any](as []ReaderIOEither[R, E, A], m monoid.Monoid[A]) ReaderIOEither[R, E, A] {
|
||||
return MonadReduceArray(as, m.Concat, m.Empty())
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ReduceArrayM[R, E, A any](m monoid.Monoid[A]) Kleisli[R, E, []ReaderIOEither[R, E, A], A] {
|
||||
return ReduceArray[R, E](m.Concat, m.Empty())
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadTraverseReduceArray[R, E, A, B, C any](as []A, trfrm Kleisli[R, E, A, B], reduce func(C, B) C, initial C) ReaderIOEither[R, E, C] {
|
||||
return RA.MonadTraverseReduce(
|
||||
Of,
|
||||
Map,
|
||||
Ap,
|
||||
|
||||
as,
|
||||
|
||||
trfrm,
|
||||
reduce,
|
||||
initial,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TraverseReduceArray[R, E, A, B, C any](trfrm Kleisli[R, E, A, B], reduce func(C, B) C, initial C) Kleisli[R, E, []A, C] {
|
||||
return RA.TraverseReduce[[]A](
|
||||
Of,
|
||||
Map,
|
||||
Ap,
|
||||
|
||||
trfrm,
|
||||
reduce,
|
||||
initial,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadTraverseReduceArrayM[R, E, A, B any](as []A, trfrm Kleisli[R, E, A, B], m monoid.Monoid[B]) ReaderIOEither[R, E, B] {
|
||||
return MonadTraverseReduceArray(as, trfrm, m.Concat, m.Empty())
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TraverseReduceArrayM[R, E, A, B any](trfrm Kleisli[R, E, A, B], m monoid.Monoid[B]) Kleisli[R, E, []A, B] {
|
||||
return TraverseReduceArray(trfrm, m.Concat, m.Empty())
|
||||
}
|
||||
296
v2/readerioresult/array.go
Normal file
296
v2/readerioresult/array.go
Normal file
@@ -0,0 +1,296 @@
|
||||
// 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 readerioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
RA "github.com/IBM/fp-go/v2/internal/array"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
)
|
||||
|
||||
// MonadReduceArray reduces an array of ReaderIOResults to a single ReaderIOResult by applying a reduction function.
|
||||
// This is the monadic version that takes the array of ReaderIOResults as the first parameter.
|
||||
//
|
||||
// Each ReaderIOResult is evaluated with the same environment R, and the results are accumulated using
|
||||
// the provided reduce function starting from the initial value. If any ReaderIOResult fails, the entire
|
||||
// operation fails with that error.
|
||||
//
|
||||
// Parameters:
|
||||
// - as: Array of ReaderIOResults to reduce
|
||||
// - reduce: Binary function that combines accumulated value with each ReaderIOResult's result
|
||||
// - initial: Starting value for the reduction
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Base int }
|
||||
// readers := []readerioresult.ReaderIOResult[Config, int]{
|
||||
// readerioresult.Of[Config](func(c Config) int { return c.Base + 1 }),
|
||||
// readerioresult.Of[Config](func(c Config) int { return c.Base + 2 }),
|
||||
// readerioresult.Of[Config](func(c Config) int { return c.Base + 3 }),
|
||||
// }
|
||||
// sum := func(acc, val int) int { return acc + val }
|
||||
// r := readerioresult.MonadReduceArray(readers, sum, 0)
|
||||
// result := r(Config{Base: 10})() // result.Of(36) (11 + 12 + 13)
|
||||
//
|
||||
//go:inline
|
||||
func MonadReduceArray[R, A, B any](as []ReaderIOResult[R, A], reduce func(B, A) B, initial B) ReaderIOResult[R, B] {
|
||||
return RA.MonadTraverseReduce(
|
||||
Of,
|
||||
Map,
|
||||
Ap,
|
||||
|
||||
as,
|
||||
|
||||
function.Identity[ReaderIOResult[R, A]],
|
||||
reduce,
|
||||
initial,
|
||||
)
|
||||
}
|
||||
|
||||
// ReduceArray returns a curried function that reduces an array of ReaderIOResults to a single ReaderIOResult.
|
||||
// This is the curried version where the reduction function and initial value are provided first,
|
||||
// returning a function that takes the array of ReaderIOResults.
|
||||
//
|
||||
// Parameters:
|
||||
// - reduce: Binary function that combines accumulated value with each ReaderIOResult's result
|
||||
// - initial: Starting value for the reduction
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an array of ReaderIOResults and returns a ReaderIOResult of the reduced result
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Multiplier int }
|
||||
// product := func(acc, val int) int { return acc * val }
|
||||
// reducer := readerioresult.ReduceArray[Config](product, 1)
|
||||
// readers := []readerioresult.ReaderIOResult[Config, int]{
|
||||
// readerioresult.Of[Config](func(c Config) int { return c.Multiplier * 2 }),
|
||||
// readerioresult.Of[Config](func(c Config) int { return c.Multiplier * 3 }),
|
||||
// }
|
||||
// r := reducer(readers)
|
||||
// result := r(Config{Multiplier: 5})() // result.Of(150) (10 * 15)
|
||||
//
|
||||
//go:inline
|
||||
func ReduceArray[R, A, B any](reduce func(B, A) B, initial B) Kleisli[R, []ReaderIOResult[R, A], B] {
|
||||
return RA.TraverseReduce[[]ReaderIOResult[R, A]](
|
||||
Of,
|
||||
Map,
|
||||
Ap,
|
||||
|
||||
function.Identity[ReaderIOResult[R, A]],
|
||||
reduce,
|
||||
initial,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadReduceArrayM reduces an array of ReaderIOResults using a Monoid to combine the results.
|
||||
// This is the monadic version that takes the array of ReaderIOResults as the first parameter.
|
||||
//
|
||||
// The Monoid provides both the binary operation (Concat) and the identity element (Empty)
|
||||
// for the reduction, making it convenient when working with monoidal types. If any ReaderIOResult
|
||||
// fails, the entire operation fails with that error.
|
||||
//
|
||||
// Parameters:
|
||||
// - as: Array of ReaderIOResults to reduce
|
||||
// - m: Monoid that defines how to combine the ReaderIOResult results
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Factor int }
|
||||
// readers := []readerioresult.ReaderIOResult[Config, int]{
|
||||
// readerioresult.Of[Config](func(c Config) int { return c.Factor }),
|
||||
// readerioresult.Of[Config](func(c Config) int { return c.Factor * 2 }),
|
||||
// readerioresult.Of[Config](func(c Config) int { return c.Factor * 3 }),
|
||||
// }
|
||||
// intAddMonoid := monoid.MakeMonoid(func(a, b int) int { return a + b }, 0)
|
||||
// r := readerioresult.MonadReduceArrayM(readers, intAddMonoid)
|
||||
// result := r(Config{Factor: 5})() // result.Of(30) (5 + 10 + 15)
|
||||
//
|
||||
//go:inline
|
||||
func MonadReduceArrayM[R, A any](as []ReaderIOResult[R, A], m monoid.Monoid[A]) ReaderIOResult[R, A] {
|
||||
return MonadReduceArray(as, m.Concat, m.Empty())
|
||||
}
|
||||
|
||||
// ReduceArrayM returns a curried function that reduces an array of ReaderIOResults using a Monoid.
|
||||
// This is the curried version where the Monoid is provided first, returning a function
|
||||
// that takes the array of ReaderIOResults.
|
||||
//
|
||||
// The Monoid provides both the binary operation (Concat) and the identity element (Empty)
|
||||
// for the reduction.
|
||||
//
|
||||
// Parameters:
|
||||
// - m: Monoid that defines how to combine the ReaderIOResult results
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an array of ReaderIOResults and returns a ReaderIOResult of the reduced result
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Scale int }
|
||||
// intMultMonoid := monoid.MakeMonoid(func(a, b int) int { return a * b }, 1)
|
||||
// reducer := readerioresult.ReduceArrayM[Config](intMultMonoid)
|
||||
// readers := []readerioresult.ReaderIOResult[Config, int]{
|
||||
// readerioresult.Of[Config](func(c Config) int { return c.Scale }),
|
||||
// readerioresult.Of[Config](func(c Config) int { return c.Scale * 2 }),
|
||||
// }
|
||||
// r := reducer(readers)
|
||||
// result := r(Config{Scale: 3})() // result.Of(18) (3 * 6)
|
||||
//
|
||||
//go:inline
|
||||
func ReduceArrayM[R, A any](m monoid.Monoid[A]) Kleisli[R, []ReaderIOResult[R, A], A] {
|
||||
return ReduceArray[R](m.Concat, m.Empty())
|
||||
}
|
||||
|
||||
// MonadTraverseReduceArray transforms and reduces an array in one operation.
|
||||
// This is the monadic version that takes the array as the first parameter.
|
||||
//
|
||||
// First, each element is transformed using the provided Kleisli function into a ReaderIOResult.
|
||||
// Then, the ReaderIOResult results are reduced using the provided reduction function.
|
||||
// If any transformation fails, the entire operation fails with that error.
|
||||
//
|
||||
// This is more efficient than calling TraverseArray followed by a separate reduce operation,
|
||||
// as it combines both operations into a single traversal.
|
||||
//
|
||||
// Parameters:
|
||||
// - as: Array of elements to transform and reduce
|
||||
// - trfrm: Function that transforms each element into a ReaderIOResult
|
||||
// - reduce: Binary function that combines accumulated value with each transformed result
|
||||
// - initial: Starting value for the reduction
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Multiplier int }
|
||||
// numbers := []int{1, 2, 3, 4}
|
||||
// multiply := func(n int) readerioresult.ReaderIOResult[Config, int] {
|
||||
// return readerioresult.Of[Config](func(c Config) int { return n * c.Multiplier })
|
||||
// }
|
||||
// sum := func(acc, val int) int { return acc + val }
|
||||
// r := readerioresult.MonadTraverseReduceArray(numbers, multiply, sum, 0)
|
||||
// result := r(Config{Multiplier: 10})() // result.Of(100) (10 + 20 + 30 + 40)
|
||||
//
|
||||
//go:inline
|
||||
func MonadTraverseReduceArray[R, A, B, C any](as []A, trfrm Kleisli[R, A, B], reduce func(C, B) C, initial C) ReaderIOResult[R, C] {
|
||||
return RA.MonadTraverseReduce(
|
||||
Of,
|
||||
Map,
|
||||
Ap,
|
||||
|
||||
as,
|
||||
|
||||
trfrm,
|
||||
reduce,
|
||||
initial,
|
||||
)
|
||||
}
|
||||
|
||||
// TraverseReduceArray returns a curried function that transforms and reduces an array.
|
||||
// This is the curried version where the transformation function, reduce function, and initial value
|
||||
// are provided first, returning a function that takes the array.
|
||||
//
|
||||
// First, each element is transformed using the provided Kleisli function into a ReaderIOResult.
|
||||
// Then, the ReaderIOResult results are reduced using the provided reduction function.
|
||||
//
|
||||
// Parameters:
|
||||
// - trfrm: Function that transforms each element into a ReaderIOResult
|
||||
// - reduce: Binary function that combines accumulated value with each transformed result
|
||||
// - initial: Starting value for the reduction
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an array and returns a ReaderIOResult of the reduced result
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Base int }
|
||||
// addBase := func(n int) readerioresult.ReaderIOResult[Config, int] {
|
||||
// return readerioresult.Of[Config](func(c Config) int { return n + c.Base })
|
||||
// }
|
||||
// product := func(acc, val int) int { return acc * val }
|
||||
// transformer := readerioresult.TraverseReduceArray(addBase, product, 1)
|
||||
// r := transformer([]int{2, 3, 4})
|
||||
// result := r(Config{Base: 10})() // result.Of(2184) (12 * 13 * 14)
|
||||
//
|
||||
//go:inline
|
||||
func TraverseReduceArray[R, A, B, C any](trfrm Kleisli[R, A, B], reduce func(C, B) C, initial C) Kleisli[R, []A, C] {
|
||||
return RA.TraverseReduce[[]A](
|
||||
Of,
|
||||
Map,
|
||||
Ap,
|
||||
|
||||
trfrm,
|
||||
reduce,
|
||||
initial,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadTraverseReduceArrayM transforms and reduces an array using a Monoid.
|
||||
// This is the monadic version that takes the array as the first parameter.
|
||||
//
|
||||
// First, each element is transformed using the provided Kleisli function into a ReaderIOResult.
|
||||
// Then, the ReaderIOResult results are reduced using the Monoid's binary operation and identity element.
|
||||
// If any transformation fails, the entire operation fails with that error.
|
||||
//
|
||||
// This combines transformation and monoidal reduction in a single efficient operation.
|
||||
//
|
||||
// Parameters:
|
||||
// - as: Array of elements to transform and reduce
|
||||
// - trfrm: Function that transforms each element into a ReaderIOResult
|
||||
// - m: Monoid that defines how to combine the transformed results
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Offset int }
|
||||
// numbers := []int{1, 2, 3}
|
||||
// addOffset := func(n int) readerioresult.ReaderIOResult[Config, int] {
|
||||
// return readerioresult.Of[Config](func(c Config) int { return n + c.Offset })
|
||||
// }
|
||||
// intSumMonoid := monoid.MakeMonoid(func(a, b int) int { return a + b }, 0)
|
||||
// r := readerioresult.MonadTraverseReduceArrayM(numbers, addOffset, intSumMonoid)
|
||||
// result := r(Config{Offset: 100})() // result.Of(306) (101 + 102 + 103)
|
||||
//
|
||||
//go:inline
|
||||
func MonadTraverseReduceArrayM[R, A, B any](as []A, trfrm Kleisli[R, A, B], m monoid.Monoid[B]) ReaderIOResult[R, B] {
|
||||
return MonadTraverseReduceArray(as, trfrm, m.Concat, m.Empty())
|
||||
}
|
||||
|
||||
// TraverseReduceArrayM returns a curried function that transforms and reduces an array using a Monoid.
|
||||
// This is the curried version where the transformation function and Monoid are provided first,
|
||||
// returning a function that takes the array.
|
||||
//
|
||||
// First, each element is transformed using the provided Kleisli function into a ReaderIOResult.
|
||||
// Then, the ReaderIOResult results are reduced using the Monoid's binary operation and identity element.
|
||||
//
|
||||
// Parameters:
|
||||
// - trfrm: Function that transforms each element into a ReaderIOResult
|
||||
// - m: Monoid that defines how to combine the transformed results
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an array and returns a ReaderIOResult of the reduced result
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Factor int }
|
||||
// scale := func(n int) readerioresult.ReaderIOResult[Config, int] {
|
||||
// return readerioresult.Of[Config](func(c Config) int { return n * c.Factor })
|
||||
// }
|
||||
// intProdMonoid := monoid.MakeMonoid(func(a, b int) int { return a * b }, 1)
|
||||
// transformer := readerioresult.TraverseReduceArrayM(scale, intProdMonoid)
|
||||
// r := transformer([]int{2, 3, 4})
|
||||
// result := r(Config{Factor: 5})() // result.Of(3000) (10 * 15 * 20)
|
||||
//
|
||||
//go:inline
|
||||
func TraverseReduceArrayM[R, A, B any](trfrm Kleisli[R, A, B], m monoid.Monoid[B]) Kleisli[R, []A, B] {
|
||||
return TraverseReduceArray(trfrm, m.Concat, m.Empty())
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
TST "github.com/IBM/fp-go/v2/internal/testing"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -73,3 +74,341 @@ func TestSequenceArrayError(t *testing.T) {
|
||||
// run across four bits
|
||||
s(4)(t)
|
||||
}
|
||||
|
||||
func TestMonadReduceArray(t *testing.T) {
|
||||
type Config struct{ Base int }
|
||||
config := Config{Base: 10}
|
||||
|
||||
readers := []ReaderIOResult[Config, int]{
|
||||
Of[Config](11),
|
||||
Of[Config](12),
|
||||
Of[Config](13),
|
||||
}
|
||||
|
||||
sum := func(acc, val int) int { return acc + val }
|
||||
r := MonadReduceArray(readers, sum, 0)
|
||||
res := r(config)()
|
||||
|
||||
assert.Equal(t, result.Of(36), res) // 11 + 12 + 13
|
||||
}
|
||||
|
||||
func TestMonadReduceArrayWithError(t *testing.T) {
|
||||
type Config struct{ Base int }
|
||||
config := Config{Base: 10}
|
||||
|
||||
testErr := errors.New("test error")
|
||||
readers := []ReaderIOResult[Config, int]{
|
||||
Of[Config](11),
|
||||
Left[Config, int](testErr),
|
||||
Of[Config](13),
|
||||
}
|
||||
|
||||
sum := func(acc, val int) int { return acc + val }
|
||||
r := MonadReduceArray(readers, sum, 0)
|
||||
res := r(config)()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
val, err := result.Unwrap(res)
|
||||
assert.Equal(t, 0, val)
|
||||
assert.Equal(t, testErr, err)
|
||||
}
|
||||
|
||||
func TestReduceArray(t *testing.T) {
|
||||
type Config struct{ Multiplier int }
|
||||
config := Config{Multiplier: 5}
|
||||
|
||||
product := func(acc, val int) int { return acc * val }
|
||||
reducer := ReduceArray[Config](product, 1)
|
||||
|
||||
readers := []ReaderIOResult[Config, int]{
|
||||
Of[Config](10),
|
||||
Of[Config](15),
|
||||
}
|
||||
|
||||
r := reducer(readers)
|
||||
res := r(config)()
|
||||
|
||||
assert.Equal(t, result.Of(150), res) // 10 * 15
|
||||
}
|
||||
|
||||
func TestReduceArrayWithError(t *testing.T) {
|
||||
type Config struct{ Multiplier int }
|
||||
config := Config{Multiplier: 5}
|
||||
|
||||
testErr := errors.New("multiplication error")
|
||||
product := func(acc, val int) int { return acc * val }
|
||||
reducer := ReduceArray[Config](product, 1)
|
||||
|
||||
readers := []ReaderIOResult[Config, int]{
|
||||
Of[Config](10),
|
||||
Left[Config, int](testErr),
|
||||
}
|
||||
|
||||
r := reducer(readers)
|
||||
res := r(config)()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
_, err := result.Unwrap(res)
|
||||
assert.Equal(t, testErr, err)
|
||||
}
|
||||
|
||||
func TestMonadReduceArrayM(t *testing.T) {
|
||||
type Config struct{ Factor int }
|
||||
config := Config{Factor: 5}
|
||||
|
||||
readers := []ReaderIOResult[Config, int]{
|
||||
Of[Config](5),
|
||||
Of[Config](10),
|
||||
Of[Config](15),
|
||||
}
|
||||
|
||||
intAddMonoid := M.MakeMonoid(func(a, b int) int { return a + b }, 0)
|
||||
r := MonadReduceArrayM(readers, intAddMonoid)
|
||||
res := r(config)()
|
||||
|
||||
assert.Equal(t, result.Of(30), res) // 5 + 10 + 15
|
||||
}
|
||||
|
||||
func TestMonadReduceArrayMWithError(t *testing.T) {
|
||||
type Config struct{ Factor int }
|
||||
config := Config{Factor: 5}
|
||||
|
||||
testErr := errors.New("monoid error")
|
||||
readers := []ReaderIOResult[Config, int]{
|
||||
Of[Config](5),
|
||||
Left[Config, int](testErr),
|
||||
Of[Config](15),
|
||||
}
|
||||
|
||||
intAddMonoid := M.MakeMonoid(func(a, b int) int { return a + b }, 0)
|
||||
r := MonadReduceArrayM(readers, intAddMonoid)
|
||||
res := r(config)()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
_, err := result.Unwrap(res)
|
||||
assert.Equal(t, testErr, err)
|
||||
}
|
||||
|
||||
func TestReduceArrayM(t *testing.T) {
|
||||
type Config struct{ Scale int }
|
||||
config := Config{Scale: 3}
|
||||
|
||||
intMultMonoid := M.MakeMonoid(func(a, b int) int { return a * b }, 1)
|
||||
reducer := ReduceArrayM[Config](intMultMonoid)
|
||||
|
||||
readers := []ReaderIOResult[Config, int]{
|
||||
Of[Config](3),
|
||||
Of[Config](6),
|
||||
}
|
||||
|
||||
r := reducer(readers)
|
||||
res := r(config)()
|
||||
|
||||
assert.Equal(t, result.Of(18), res) // 3 * 6
|
||||
}
|
||||
|
||||
func TestReduceArrayMWithError(t *testing.T) {
|
||||
type Config struct{ Scale int }
|
||||
config := Config{Scale: 3}
|
||||
|
||||
testErr := errors.New("scale error")
|
||||
intMultMonoid := M.MakeMonoid(func(a, b int) int { return a * b }, 1)
|
||||
reducer := ReduceArrayM[Config](intMultMonoid)
|
||||
|
||||
readers := []ReaderIOResult[Config, int]{
|
||||
Of[Config](3),
|
||||
Left[Config, int](testErr),
|
||||
}
|
||||
|
||||
r := reducer(readers)
|
||||
res := r(config)()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
_, err := result.Unwrap(res)
|
||||
assert.Equal(t, testErr, err)
|
||||
}
|
||||
|
||||
func TestMonadTraverseReduceArray(t *testing.T) {
|
||||
type Config struct{ Multiplier int }
|
||||
config := Config{Multiplier: 10}
|
||||
|
||||
numbers := []int{1, 2, 3, 4}
|
||||
multiply := func(n int) ReaderIOResult[Config, int] {
|
||||
return Of[Config](n * 10)
|
||||
}
|
||||
|
||||
sum := func(acc, val int) int { return acc + val }
|
||||
r := MonadTraverseReduceArray(numbers, multiply, sum, 0)
|
||||
res := r(config)()
|
||||
|
||||
assert.Equal(t, result.Of(100), res) // 10 + 20 + 30 + 40
|
||||
}
|
||||
|
||||
func TestMonadTraverseReduceArrayWithError(t *testing.T) {
|
||||
type Config struct{ Multiplier int }
|
||||
config := Config{Multiplier: 10}
|
||||
|
||||
testErr := errors.New("transform error")
|
||||
numbers := []int{1, 2, 3, 4}
|
||||
multiply := func(n int) ReaderIOResult[Config, int] {
|
||||
if n == 3 {
|
||||
return Left[Config, int](testErr)
|
||||
}
|
||||
return Of[Config](n * 10)
|
||||
}
|
||||
|
||||
sum := func(acc, val int) int { return acc + val }
|
||||
r := MonadTraverseReduceArray(numbers, multiply, sum, 0)
|
||||
res := r(config)()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
_, err := result.Unwrap(res)
|
||||
assert.Equal(t, testErr, err)
|
||||
}
|
||||
|
||||
func TestTraverseReduceArray(t *testing.T) {
|
||||
type Config struct{ Base int }
|
||||
config := Config{Base: 10}
|
||||
|
||||
addBase := func(n int) ReaderIOResult[Config, int] {
|
||||
return Of[Config](n + 10)
|
||||
}
|
||||
|
||||
product := func(acc, val int) int { return acc * val }
|
||||
transformer := TraverseReduceArray(addBase, product, 1)
|
||||
|
||||
r := transformer([]int{2, 3, 4})
|
||||
res := r(config)()
|
||||
|
||||
assert.Equal(t, result.Of(2184), res) // 12 * 13 * 14
|
||||
}
|
||||
|
||||
func TestTraverseReduceArrayWithError(t *testing.T) {
|
||||
type Config struct{ Base int }
|
||||
config := Config{Base: 10}
|
||||
|
||||
testErr := errors.New("addition error")
|
||||
addBase := func(n int) ReaderIOResult[Config, int] {
|
||||
if n == 3 {
|
||||
return Left[Config, int](testErr)
|
||||
}
|
||||
return Of[Config](n + 10)
|
||||
}
|
||||
|
||||
product := func(acc, val int) int { return acc * val }
|
||||
transformer := TraverseReduceArray(addBase, product, 1)
|
||||
|
||||
r := transformer([]int{2, 3, 4})
|
||||
res := r(config)()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
_, err := result.Unwrap(res)
|
||||
assert.Equal(t, testErr, err)
|
||||
}
|
||||
|
||||
func TestMonadTraverseReduceArrayM(t *testing.T) {
|
||||
type Config struct{ Offset int }
|
||||
config := Config{Offset: 100}
|
||||
|
||||
numbers := []int{1, 2, 3}
|
||||
addOffset := func(n int) ReaderIOResult[Config, int] {
|
||||
return Of[Config](n + 100)
|
||||
}
|
||||
|
||||
intSumMonoid := M.MakeMonoid(func(a, b int) int { return a + b }, 0)
|
||||
r := MonadTraverseReduceArrayM(numbers, addOffset, intSumMonoid)
|
||||
res := r(config)()
|
||||
|
||||
assert.Equal(t, result.Of(306), res) // 101 + 102 + 103
|
||||
}
|
||||
|
||||
func TestMonadTraverseReduceArrayMWithError(t *testing.T) {
|
||||
type Config struct{ Offset int }
|
||||
config := Config{Offset: 100}
|
||||
|
||||
testErr := errors.New("offset error")
|
||||
numbers := []int{1, 2, 3}
|
||||
addOffset := func(n int) ReaderIOResult[Config, int] {
|
||||
if n == 2 {
|
||||
return Left[Config, int](testErr)
|
||||
}
|
||||
return Of[Config](n + 100)
|
||||
}
|
||||
|
||||
intSumMonoid := M.MakeMonoid(func(a, b int) int { return a + b }, 0)
|
||||
r := MonadTraverseReduceArrayM(numbers, addOffset, intSumMonoid)
|
||||
res := r(config)()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
_, err := result.Unwrap(res)
|
||||
assert.Equal(t, testErr, err)
|
||||
}
|
||||
|
||||
func TestTraverseReduceArrayM(t *testing.T) {
|
||||
type Config struct{ Factor int }
|
||||
config := Config{Factor: 5}
|
||||
|
||||
scale := func(n int) ReaderIOResult[Config, int] {
|
||||
return Of[Config](n * 5)
|
||||
}
|
||||
|
||||
intProdMonoid := M.MakeMonoid(func(a, b int) int { return a * b }, 1)
|
||||
transformer := TraverseReduceArrayM(scale, intProdMonoid)
|
||||
r := transformer([]int{2, 3, 4})
|
||||
res := r(config)()
|
||||
|
||||
assert.Equal(t, result.Of(3000), res) // 10 * 15 * 20
|
||||
}
|
||||
|
||||
func TestTraverseReduceArrayMWithError(t *testing.T) {
|
||||
type Config struct{ Factor int }
|
||||
config := Config{Factor: 5}
|
||||
|
||||
testErr := errors.New("scaling error")
|
||||
scale := func(n int) ReaderIOResult[Config, int] {
|
||||
if n == 3 {
|
||||
return Left[Config, int](testErr)
|
||||
}
|
||||
return Of[Config](n * 5)
|
||||
}
|
||||
|
||||
intProdMonoid := M.MakeMonoid(func(a, b int) int { return a * b }, 1)
|
||||
transformer := TraverseReduceArrayM(scale, intProdMonoid)
|
||||
r := transformer([]int{2, 3, 4})
|
||||
res := r(config)()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
_, err := result.Unwrap(res)
|
||||
assert.Equal(t, testErr, err)
|
||||
}
|
||||
|
||||
func TestReduceArrayEmptyArray(t *testing.T) {
|
||||
type Config struct{ Base int }
|
||||
config := Config{Base: 10}
|
||||
|
||||
sum := func(acc, val int) int { return acc + val }
|
||||
reducer := ReduceArray[Config](sum, 100)
|
||||
|
||||
readers := []ReaderIOResult[Config, int]{}
|
||||
r := reducer(readers)
|
||||
res := r(config)()
|
||||
|
||||
assert.Equal(t, result.Of(100), res) // Should return initial value
|
||||
}
|
||||
|
||||
func TestTraverseReduceArrayEmptyArray(t *testing.T) {
|
||||
type Config struct{ Base int }
|
||||
config := Config{Base: 10}
|
||||
|
||||
addBase := func(n int) ReaderIOResult[Config, int] {
|
||||
return Of[Config](n + 10)
|
||||
}
|
||||
|
||||
sum := func(acc, val int) int { return acc + val }
|
||||
transformer := TraverseReduceArray(addBase, sum, 50)
|
||||
|
||||
r := transformer([]int{})
|
||||
res := r(config)()
|
||||
|
||||
assert.Equal(t, result.Of(50), res) // Should return initial value
|
||||
}
|
||||
|
||||
287
v2/readerresult/benchmark_test.go
Normal file
287
v2/readerresult/benchmark_test.go
Normal file
@@ -0,0 +1,287 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
type BenchContext struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
var benchError = errors.New("benchmark error")
|
||||
|
||||
// Benchmark basic operations
|
||||
|
||||
func BenchmarkOf(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
rr := Of[BenchContext](i)
|
||||
_ = rr(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLeft(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
err := benchError
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
rr := Left[BenchContext, int](err)
|
||||
_ = rr(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMap(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr := Of[BenchContext](10)
|
||||
double := N.Mul(2)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
mapped := F.Pipe1(rr, Map[BenchContext](double))
|
||||
_ = mapped(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMapChain(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr := Of[BenchContext](1)
|
||||
double := N.Mul(2)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := F.Pipe3(
|
||||
rr,
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
)
|
||||
_ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChain(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr := Of[BenchContext](10)
|
||||
addOne := func(x int) ReaderResult[BenchContext, int] {
|
||||
return Of[BenchContext](x + 1)
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
chained := F.Pipe1(rr, Chain(addOne))
|
||||
_ = chained(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChainDeep(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr := Of[BenchContext](1)
|
||||
addOne := func(x int) ReaderResult[BenchContext, int] {
|
||||
return Of[BenchContext](x + 1)
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := F.Pipe5(
|
||||
rr,
|
||||
Chain(addOne),
|
||||
Chain(addOne),
|
||||
Chain(addOne),
|
||||
Chain(addOne),
|
||||
Chain(addOne),
|
||||
)
|
||||
_ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAp(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
fab := Of[BenchContext](N.Mul(2))
|
||||
fa := Of[BenchContext](21)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := MonadAp(fab, fa)
|
||||
_ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSequenceT2(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr1 := Of[BenchContext](10)
|
||||
rr2 := Of[BenchContext](20)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := SequenceT2(rr1, rr2)
|
||||
_ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSequenceT4(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr1 := Of[BenchContext](10)
|
||||
rr2 := Of[BenchContext](20)
|
||||
rr3 := Of[BenchContext](30)
|
||||
rr4 := Of[BenchContext](40)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := SequenceT4(rr1, rr2, rr3, rr4)
|
||||
_ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDoNotation(b *testing.B) {
|
||||
ctx := context.Background()
|
||||
|
||||
type State struct {
|
||||
A int
|
||||
B int
|
||||
C int
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := F.Pipe3(
|
||||
Do[context.Context](State{}),
|
||||
Bind(
|
||||
func(a int) func(State) State {
|
||||
return func(s State) State { s.A = a; return s }
|
||||
},
|
||||
func(s State) ReaderResult[context.Context, int] {
|
||||
return Of[context.Context](10)
|
||||
},
|
||||
),
|
||||
Bind(
|
||||
func(b int) func(State) State {
|
||||
return func(s State) State { s.B = b; return s }
|
||||
},
|
||||
func(s State) ReaderResult[context.Context, int] {
|
||||
return Of[context.Context](s.A * 2)
|
||||
},
|
||||
),
|
||||
Bind(
|
||||
func(c int) func(State) State {
|
||||
return func(s State) State { s.C = c; return s }
|
||||
},
|
||||
func(s State) ReaderResult[context.Context, int] {
|
||||
return Of[context.Context](s.A + s.B)
|
||||
},
|
||||
),
|
||||
)
|
||||
_ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkErrorPropagation(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
err := benchError
|
||||
rr := Left[BenchContext, int](err)
|
||||
double := N.Mul(2)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := F.Pipe5(
|
||||
rr,
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
)
|
||||
_ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTraverseArray(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
arr := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
kleisli := func(x int) ReaderResult[BenchContext, int] {
|
||||
return Of[BenchContext](x * 2)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
traversed := TraverseArray(kleisli)
|
||||
result := traversed(arr)
|
||||
_ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSequenceArray(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
arr := []ReaderResult[BenchContext, int]{
|
||||
Of[BenchContext](1),
|
||||
Of[BenchContext](2),
|
||||
Of[BenchContext](3),
|
||||
Of[BenchContext](4),
|
||||
Of[BenchContext](5),
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := SequenceArray(arr)
|
||||
_ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Real-world scenario benchmarks
|
||||
|
||||
func BenchmarkRealWorldPipeline(b *testing.B) {
|
||||
type Config struct {
|
||||
Multiplier int
|
||||
Offset int
|
||||
}
|
||||
|
||||
ctx := Config{Multiplier: 5, Offset: 10}
|
||||
|
||||
type State struct {
|
||||
Input int
|
||||
Result int
|
||||
}
|
||||
|
||||
getMultiplier := func(cfg Config) int { return cfg.Multiplier }
|
||||
getOffset := func(cfg Config) int { return cfg.Offset }
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
step1 := Bind(
|
||||
func(m int) func(State) State {
|
||||
return func(s State) State { s.Result = s.Input * m; return s }
|
||||
},
|
||||
func(s State) ReaderResult[Config, int] {
|
||||
return Asks(getMultiplier)
|
||||
},
|
||||
)
|
||||
step2 := Bind(
|
||||
func(off int) func(State) State {
|
||||
return func(s State) State { s.Result += off; return s }
|
||||
},
|
||||
func(s State) ReaderResult[Config, int] {
|
||||
return Asks(getOffset)
|
||||
},
|
||||
)
|
||||
result := F.Pipe3(
|
||||
Do[Config](State{Input: 10}),
|
||||
step1,
|
||||
step2,
|
||||
Map[Config](func(s State) int { return s.Result }),
|
||||
)
|
||||
_ = result(ctx)
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ package readerresult
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
RRI "github.com/IBM/fp-go/v2/idiomatic/readerresult"
|
||||
RI "github.com/IBM/fp-go/v2/idiomatic/result"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
G "github.com/IBM/fp-go/v2/readereither/generic"
|
||||
@@ -92,17 +94,57 @@ func Do[R, S any](
|
||||
func Bind[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f Kleisli[R, S1, T],
|
||||
) func(ReaderResult[R, S1]) ReaderResult[R, S2] {
|
||||
) Operator[R, S1, S2] {
|
||||
return G.Bind[ReaderResult[R, S1], ReaderResult[R, S2]](setter, f)
|
||||
}
|
||||
|
||||
// BindI attaches the result of an idiomatic computation to a context [S1] to produce a context [S2].
|
||||
// This is the idiomatic version of Bind, where the computation returns (T, error) instead of Result[T].
|
||||
// This enables sequential composition with Go's native error handling style where each step can depend
|
||||
// on the results of previous steps and access the shared environment.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
// type Env struct {
|
||||
// UserService UserService
|
||||
// }
|
||||
//
|
||||
// // Idiomatic function returning (User, error)
|
||||
// getUser := func(s State) func(env Env) (User, error) {
|
||||
// return func(env Env) (User, error) {
|
||||
// return env.UserService.GetUser()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// readerresult.Do[Env](State{}),
|
||||
// readerresult.BindI(
|
||||
// func(user User) func(State) State {
|
||||
// return func(s State) State { s.User = user; return s }
|
||||
// },
|
||||
// getUser,
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func BindI[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f RRI.Kleisli[R, S1, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return Bind(setter, fromReaderResultKleisliI(f))
|
||||
}
|
||||
|
||||
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
//
|
||||
//go:inline
|
||||
func Let[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
) func(ReaderResult[R, S1]) ReaderResult[R, S2] {
|
||||
) Operator[R, S1, S2] {
|
||||
return G.Let[ReaderResult[R, S1], ReaderResult[R, S2]](setter, f)
|
||||
}
|
||||
|
||||
@@ -112,7 +154,7 @@ func Let[R, S1, S2, T any](
|
||||
func LetTo[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
) func(ReaderResult[R, S1]) ReaderResult[R, S2] {
|
||||
) Operator[R, S1, S2] {
|
||||
return G.LetTo[ReaderResult[R, S1], ReaderResult[R, S2]](setter, b)
|
||||
}
|
||||
|
||||
@@ -171,10 +213,48 @@ func BindTo[R, S1, T any](
|
||||
func ApS[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa ReaderResult[R, T],
|
||||
) func(ReaderResult[R, S1]) ReaderResult[R, S2] {
|
||||
) Operator[R, S1, S2] {
|
||||
return G.ApS[ReaderResult[R, S1], ReaderResult[R, S2]](setter, fa)
|
||||
}
|
||||
|
||||
// ApIS attaches a value from an idiomatic ReaderResult to a context [S1] to produce a context [S2].
|
||||
// This is the idiomatic version of ApS, where the computation returns (T, error) instead of Result[T].
|
||||
// Unlike BindI which sequences operations, ApIS uses applicative semantics, meaning the computation
|
||||
// is independent of the current state and can conceptually run in parallel.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
// type Env struct {
|
||||
// UserService UserService
|
||||
// }
|
||||
//
|
||||
// // Idiomatic independent computation returning (User, error)
|
||||
// getUser := func(env Env) (User, error) {
|
||||
// return env.UserService.GetUser()
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// readerresult.Do[Env](State{}),
|
||||
// readerresult.ApIS(
|
||||
// func(user User) func(State) State {
|
||||
// return func(s State) State { s.User = user; return s }
|
||||
// },
|
||||
// getUser,
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApIS[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa RRI.ReaderResult[R, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return ApS(setter, FromReaderResultI(fa))
|
||||
}
|
||||
|
||||
// ApSL attaches a value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines ApS with a lens, allowing you to use
|
||||
// optics to update nested structures in a more composable way.
|
||||
@@ -214,6 +294,43 @@ func ApSL[R, S, T any](
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// ApISL attaches a value from an idiomatic ReaderResult to a context using a lens-based setter.
|
||||
// This is the idiomatic version of ApSL, where the computation returns (T, error) instead of Result[T].
|
||||
// It combines ApIS with a lens, allowing you to use optics to update nested structures in a more composable way.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
// type Env struct {
|
||||
// ConfigService ConfigService
|
||||
// }
|
||||
//
|
||||
// configLens := lens.MakeLens(
|
||||
// func(s State) Config { return s.Config },
|
||||
// func(s State, c Config) State { s.Config = c; return s },
|
||||
// )
|
||||
//
|
||||
// // Idiomatic computation returning (Config, error)
|
||||
// getConfig := func(env Env) (Config, error) {
|
||||
// return env.ConfigService.GetConfig()
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// readerresult.Of[Env](State{}),
|
||||
// readerresult.ApISL(configLens, getConfig),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApISL[R, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa RRI.ReaderResult[R, T],
|
||||
) Operator[R, S, S] {
|
||||
return ApS(lens.Set, FromReaderResultI(fa))
|
||||
}
|
||||
|
||||
// BindL is a variant of Bind that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
@@ -255,6 +372,46 @@ func BindL[R, S, T any](
|
||||
return Bind(lens.Set, F.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// BindIL is a variant of BindI that uses a lens to focus on a specific part of the context.
|
||||
// This is the idiomatic version of BindL, where the computation returns (T, error) instead of Result[T].
|
||||
// It provides a more ergonomic API when working with nested structures, eliminating the need to manually
|
||||
// write setter functions while supporting Go's native error handling pattern.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
// type Env struct {
|
||||
// UserService UserService
|
||||
// }
|
||||
//
|
||||
// userLens := lens.MakeLens(
|
||||
// func(s State) User { return s.User },
|
||||
// func(s State, u User) State { s.User = u; return s },
|
||||
// )
|
||||
//
|
||||
// // Idiomatic function returning (User, error)
|
||||
// updateUser := func(user User) func(env Env) (User, error) {
|
||||
// return func(env Env) (User, error) {
|
||||
// return env.UserService.UpdateUser(user)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// readerresult.Do[Env](State{}),
|
||||
// readerresult.BindIL(userLens, updateUser),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func BindIL[R, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f RRI.Kleisli[R, T, T],
|
||||
) Operator[R, S, S] {
|
||||
return Bind(lens.Set, F.Flow3(lens.Get, f, FromReaderResultI[R, T]))
|
||||
}
|
||||
|
||||
// LetL is a variant of Let that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
@@ -373,6 +530,44 @@ func BindEitherK[R, S1, S2, T any](
|
||||
return G.BindEitherK[ReaderResult[R, S1], ReaderResult[R, S2]](setter, f)
|
||||
}
|
||||
|
||||
// BindEitherIK lifts an idiomatic Result Kleisli arrow into a ReaderResult context and binds it to the state.
|
||||
// This is the idiomatic version of BindEitherK, where the function returns (T, error) instead of Result[T].
|
||||
// It allows you to integrate idiomatic Result computations (that may fail but don't need environment access)
|
||||
// into a ReaderResult computation chain.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Value int
|
||||
// ParsedValue int
|
||||
// }
|
||||
//
|
||||
// // Idiomatic function returning (int, error)
|
||||
// parseValue := func(s State) (int, error) {
|
||||
// if s.Value < 0 {
|
||||
// return 0, errors.New("negative value")
|
||||
// }
|
||||
// return s.Value * 2, nil
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// readerresult.Do[context.Context](State{Value: 5}),
|
||||
// readerresult.BindEitherIK[context.Context](
|
||||
// func(parsed int) func(State) State {
|
||||
// return func(s State) State { s.ParsedValue = parsed; return s }
|
||||
// },
|
||||
// parseValue,
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func BindEitherIK[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f RI.Kleisli[S1, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return BindEitherK[R](setter, fromResultKleisliI(f))
|
||||
}
|
||||
|
||||
// BindResultK lifts a Result Kleisli arrow into a ReaderResult context and binds it to the state.
|
||||
// This allows you to integrate Result computations (that may fail with an error but don't need
|
||||
// environment access) into a ReaderResult computation chain.
|
||||
@@ -414,6 +609,18 @@ func BindResultK[R, S1, S2, T any](
|
||||
return G.BindEitherK[ReaderResult[R, S1], ReaderResult[R, S2]](setter, f)
|
||||
}
|
||||
|
||||
// BindResultIK is an alias for BindEitherIK.
|
||||
// It lifts an idiomatic Result Kleisli arrow into a ReaderResult context and binds it to the state.
|
||||
// The function f returns (T, error) in Go's idiomatic style.
|
||||
//
|
||||
//go:inline
|
||||
func BindResultIK[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f RI.Kleisli[S1, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return BindResultK[R](setter, fromResultKleisliI(f))
|
||||
}
|
||||
|
||||
// BindToReader initializes a new state S1 from a Reader[R, T] computation.
|
||||
// This is used to start a ReaderResult computation chain from a pure Reader value.
|
||||
//
|
||||
@@ -458,6 +665,36 @@ func BindToEither[
|
||||
return G.BindToEither[ReaderResult[R, S1]](setter)
|
||||
}
|
||||
|
||||
// BindToEitherI initializes a new state S1 from an idiomatic (value, error) pair.
|
||||
// This is the idiomatic version of BindToEither, accepting Go's native error handling pattern.
|
||||
// It's used to start a ReaderResult computation chain from an idiomatic Result that may contain an error.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// // Idiomatic result from parsing
|
||||
// value, err := strconv.Atoi("42")
|
||||
//
|
||||
// computation := readerresult.BindToEitherI[context.Context](
|
||||
// func(value int) State {
|
||||
// return State{Value: value}
|
||||
// },
|
||||
// )(value, err)
|
||||
//
|
||||
//go:inline
|
||||
func BindToEitherI[
|
||||
R, S1, T any](
|
||||
setter func(T) S1,
|
||||
) func(T, error) ReaderResult[R, S1] {
|
||||
bte := BindToEither[R](setter)
|
||||
return func(t T, err error) ReaderResult[R, S1] {
|
||||
return bte(result.TryCatchError(t, err))
|
||||
}
|
||||
}
|
||||
|
||||
// BindToResult initializes a new state S1 from a Result[T] value.
|
||||
// This is used to start a ReaderResult computation chain from a Result that may contain an error.
|
||||
//
|
||||
@@ -493,6 +730,17 @@ func BindToResult[
|
||||
return G.BindToEither[ReaderResult[R, S1]](setter)
|
||||
}
|
||||
|
||||
// BindToResultI is an alias for BindToEitherI.
|
||||
// It initializes a new state S1 from an idiomatic (value, error) pair.
|
||||
//
|
||||
//go:inline
|
||||
func BindToResultI[
|
||||
R, S1, T any](
|
||||
setter func(T) S1,
|
||||
) func(T, error) ReaderResult[R, S1] {
|
||||
return BindToEitherI[R](setter)
|
||||
}
|
||||
|
||||
// ApReaderS attaches a value from a pure Reader computation to a context [S1] to produce a context [S2]
|
||||
// using Applicative semantics (independent, non-sequential composition).
|
||||
//
|
||||
@@ -551,6 +799,39 @@ func ApEitherS[
|
||||
return G.ApEitherS[ReaderResult[R, S1], ReaderResult[R, S2]](setter, fa)
|
||||
}
|
||||
|
||||
// ApEitherIS attaches a value from an idiomatic (value, error) pair to a context [S1] to produce a context [S2].
|
||||
// This is the idiomatic version of ApEitherS, accepting Go's native error handling pattern.
|
||||
// It uses Applicative semantics (independent, non-sequential composition).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Value1 int
|
||||
// Value2 int
|
||||
// }
|
||||
//
|
||||
// // Idiomatic parsing result
|
||||
// value, err := strconv.Atoi("42")
|
||||
//
|
||||
// computation := F.Pipe1(
|
||||
// readerresult.Do[context.Context](State{}),
|
||||
// readerresult.ApEitherIS[context.Context](
|
||||
// func(v int) func(State) State {
|
||||
// return func(s State) State { s.Value1 = v; return s }
|
||||
// },
|
||||
// )(value, err),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApEitherIS[
|
||||
R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
) func(T, error) Operator[R, S1, S2] {
|
||||
return func(t T, err error) Operator[R, S1, S2] {
|
||||
return ApEitherS[R](setter, result.TryCatchError(t, err))
|
||||
}
|
||||
}
|
||||
|
||||
// ApResultS attaches a value from a Result to a context [S1] to produce a context [S2]
|
||||
// using Applicative semantics (independent, non-sequential composition).
|
||||
//
|
||||
@@ -597,3 +878,14 @@ func ApResultS[
|
||||
) Operator[R, S1, S2] {
|
||||
return G.ApEitherS[ReaderResult[R, S1], ReaderResult[R, S2]](setter, fa)
|
||||
}
|
||||
|
||||
// ApResultIS is an alias for ApEitherIS.
|
||||
// It attaches a value from an idiomatic (value, error) pair to a context [S1] to produce a context [S2].
|
||||
//
|
||||
//go:inline
|
||||
func ApResultIS[
|
||||
R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
) func(T, error) Operator[R, S1, S2] {
|
||||
return ApEitherIS[R](setter)
|
||||
}
|
||||
|
||||
1092
v2/readerresult/idiomatic_test.go
Normal file
1092
v2/readerresult/idiomatic_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -18,16 +18,32 @@ package readerresult
|
||||
import (
|
||||
ET "github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
OI "github.com/IBM/fp-go/v2/idiomatic/option"
|
||||
RRI "github.com/IBM/fp-go/v2/idiomatic/readerresult"
|
||||
RI "github.com/IBM/fp-go/v2/idiomatic/result"
|
||||
"github.com/IBM/fp-go/v2/internal/eithert"
|
||||
"github.com/IBM/fp-go/v2/internal/fromeither"
|
||||
"github.com/IBM/fp-go/v2/internal/fromreader"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/readert"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
func fromReaderResultKleisliI[R, A, B any](f RRI.Kleisli[R, A, B]) Kleisli[R, A, B] {
|
||||
return function.Flow2(f, FromReaderResultI[R, B])
|
||||
}
|
||||
|
||||
func fromResultKleisliI[A, B any](f RI.Kleisli[A, B]) result.Kleisli[A, B] {
|
||||
return result.Eitherize1(f)
|
||||
}
|
||||
|
||||
func fromOptionKleisliI[A, B any](f OI.Kleisli[A, B]) option.Kleisli[A, B] {
|
||||
return option.Optionize1(f)
|
||||
}
|
||||
|
||||
// FromEither lifts a Result[A] into a ReaderResult[R, A] that ignores the environment.
|
||||
// The resulting computation will always produce the same result regardless of the environment provided.
|
||||
//
|
||||
@@ -50,6 +66,46 @@ func FromResult[R, A any](e Result[A]) ReaderResult[R, A] {
|
||||
return reader.Of[R](e)
|
||||
}
|
||||
|
||||
// FromResultI lifts an idiomatic Go (value, error) pair into a ReaderResult[R, A] that ignores the environment.
|
||||
// This is the idiomatic version of FromResult, accepting Go's native error handling pattern.
|
||||
// If err is non-nil, the resulting computation will always fail with that error.
|
||||
// If err is nil, the resulting computation will always succeed with the value a.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// value, err := strconv.Atoi("42")
|
||||
// rr := readerresult.FromResultI[Config](value, err)
|
||||
// // rr(anyConfig) will return result.Of(42) if err is nil
|
||||
//
|
||||
//go:inline
|
||||
func FromResultI[R, A any](a A, err error) ReaderResult[R, A] {
|
||||
return reader.Of[R](result.TryCatchError(a, err))
|
||||
}
|
||||
|
||||
// FromReaderResultI converts an idiomatic ReaderResult (that returns (A, error)) into a functional ReaderResult (that returns Result[A]).
|
||||
// This bridges the gap between Go's idiomatic error handling and functional programming style.
|
||||
// The idiomatic RRI.ReaderResult[R, A] is a function R -> (A, error).
|
||||
// The functional ReaderResult[R, A] is a function R -> Result[A].
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Idiomatic ReaderResult
|
||||
// getUserID := func(cfg Config) (int, error) {
|
||||
// if cfg.Valid {
|
||||
// return 42, nil
|
||||
// }
|
||||
// return 0, errors.New("invalid config")
|
||||
// }
|
||||
// rr := readerresult.FromReaderResultI(getUserID)
|
||||
// // rr is now a functional ReaderResult[Config, int]
|
||||
//
|
||||
//go:inline
|
||||
func FromReaderResultI[R, A any](rr RRI.ReaderResult[R, A]) ReaderResult[R, A] {
|
||||
return func(r R) Result[A] {
|
||||
return result.TryCatchError(rr(r))
|
||||
}
|
||||
}
|
||||
|
||||
// RightReader lifts a Reader[R, A] into a ReaderResult[R, A] that always succeeds.
|
||||
// The resulting computation reads a value from the environment and wraps it in a successful Result.
|
||||
//
|
||||
@@ -113,7 +169,7 @@ func FromReader[R, A any](r Reader[R, A]) ReaderResult[R, A] {
|
||||
// Example:
|
||||
//
|
||||
// rr := readerresult.Of[Config](5)
|
||||
// doubled := readerresult.MonadMap(rr, func(x int) int { return x * 2 })
|
||||
// doubled := readerresult.MonadMap(rr, N.Mul(2))
|
||||
// // doubled(cfg) returns result.Of(10)
|
||||
//
|
||||
//go:inline
|
||||
@@ -126,7 +182,7 @@ func MonadMap[R, A, B any](fa ReaderResult[R, A], f func(A) B) ReaderResult[R, B
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// double := readerresult.Map[Config](func(x int) int { return x * 2 })
|
||||
// double := readerresult.Map[Config](N.Mul(2))
|
||||
// result := F.Pipe1(readerresult.Of[Config](5), double)
|
||||
//
|
||||
//go:inline
|
||||
@@ -148,6 +204,11 @@ func MonadChain[R, A, B any](ma ReaderResult[R, A], f Kleisli[R, A, B]) ReaderRe
|
||||
return readert.MonadChain(ET.MonadChain[error, A, B], ma, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadChainReaderK[R, A, B any](ma ReaderResult[R, A], f reader.Kleisli[R, A, B]) ReaderResult[R, B] {
|
||||
return readert.MonadChain(ET.MonadChain[error, A, B], ma, function.Flow2(f, FromReader[R, B]))
|
||||
}
|
||||
|
||||
// Chain is the curried version of MonadChain.
|
||||
// It returns an Operator that can be used in function composition pipelines.
|
||||
//
|
||||
@@ -161,6 +222,50 @@ func Chain[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, B] {
|
||||
return readert.Chain[ReaderResult[R, A]](ET.Chain[error, A, B], f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, B] {
|
||||
return readert.Chain[ReaderResult[R, A]](ET.Chain[error, A, B], function.Flow2(f, FromReader[R, B]))
|
||||
}
|
||||
|
||||
// MonadChainI sequences two ReaderResult computations, where the second is an idiomatic Kleisli arrow.
|
||||
// This is the idiomatic version of MonadChain, allowing you to chain with functions that return (B, error).
|
||||
// The idiomatic Kleisli arrow RRI.Kleisli[R, A, B] is a function A -> R -> (B, error).
|
||||
// If the first computation fails, the second is not executed.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getUserID := readerresult.Of[DB](42)
|
||||
// // Idiomatic function that returns (User, error)
|
||||
// fetchUser := func(id int) func(db DB) (User, error) {
|
||||
// return func(db DB) (User, error) {
|
||||
// return db.GetUser(id) // returns (User, error)
|
||||
// }
|
||||
// }
|
||||
// result := readerresult.MonadChainI(getUserID, fetchUser)
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainI[R, A, B any](ma ReaderResult[R, A], f RRI.Kleisli[R, A, B]) ReaderResult[R, B] {
|
||||
return MonadChain(ma, fromReaderResultKleisliI(f))
|
||||
}
|
||||
|
||||
// ChainI is the curried version of MonadChainI.
|
||||
// It allows chaining with idiomatic Kleisli arrows that return (B, error).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Idiomatic function that returns (User, error)
|
||||
// fetchUser := func(id int) func(db DB) (User, error) {
|
||||
// return func(db DB) (User, error) {
|
||||
// return db.GetUser(id) // returns (User, error)
|
||||
// }
|
||||
// }
|
||||
// result := F.Pipe1(getUserIDRR, readerresult.ChainI[DB](fetchUser))
|
||||
//
|
||||
//go:inline
|
||||
func ChainI[R, A, B any](f RRI.Kleisli[R, A, B]) Operator[R, A, B] {
|
||||
return Chain(fromReaderResultKleisliI(f))
|
||||
}
|
||||
|
||||
// Of creates a ReaderResult that always succeeds with the given value.
|
||||
// This is an alias for Right and is the "pure" or "return" operation for the ReaderResult monad.
|
||||
//
|
||||
@@ -190,6 +295,11 @@ func MonadAp[B, R, A any](fab ReaderResult[R, func(A) B], fa ReaderResult[R, A])
|
||||
return readert.MonadAp[ReaderResult[R, A], ReaderResult[R, B], ReaderResult[R, func(A) B], R, A](ET.MonadAp[B, error, A], fab, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadApReader[B, R, A any](fab ReaderResult[R, func(A) B], fa Reader[R, A]) ReaderResult[R, B] {
|
||||
return MonadAp(fab, FromReader(fa))
|
||||
}
|
||||
|
||||
// Ap is the curried version of MonadAp.
|
||||
// It returns an Operator that can be used in function composition pipelines.
|
||||
//
|
||||
@@ -198,6 +308,94 @@ func Ap[B, R, A any](fa ReaderResult[R, A]) Operator[R, func(A) B, B] {
|
||||
return readert.Ap[ReaderResult[R, A], ReaderResult[R, B], ReaderResult[R, func(A) B], R, A](ET.Ap[B, error, A], fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApReader[B, R, A any](fa Reader[R, A]) Operator[R, func(A) B, B] {
|
||||
return Ap[B](FromReader(fa))
|
||||
}
|
||||
|
||||
// MonadApResult applies a function wrapped in a ReaderResult to a value wrapped in a plain Result.
|
||||
// The Result value is independent of the environment, while the function may depend on it.
|
||||
// This is useful when you have a pre-computed Result value that you want to apply a context-dependent function to.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// add := func(x int) func(int) int { return func(y int) int { return x + y } }
|
||||
// fabr := readerresult.Of[Config](add(5))
|
||||
// fa := result.Of(3) // Pre-computed Result, independent of environment
|
||||
// result := readerresult.MonadApResult(fabr, fa) // Returns Of(8)
|
||||
//
|
||||
//go:inline
|
||||
func MonadApResult[B, R, A any](fab ReaderResult[R, func(A) B], fa result.Result[A]) ReaderResult[R, B] {
|
||||
return readert.MonadAp[ReaderResult[R, A], ReaderResult[R, B], ReaderResult[R, func(A) B], R, A](ET.MonadAp[B, error, A], fab, FromResult[R](fa))
|
||||
}
|
||||
|
||||
// ApResult is the curried version of MonadApResult.
|
||||
// It returns an Operator that applies a pre-computed Result value to a function in a ReaderResult context.
|
||||
// This is useful in function composition pipelines when you have a static Result value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// fa := result.Of(10)
|
||||
// result := F.Pipe1(
|
||||
// readerresult.Of[Config](utils.Double),
|
||||
// readerresult.ApResult[int, Config](fa),
|
||||
// )
|
||||
// // result(cfg) returns result.Of(20)
|
||||
//
|
||||
//go:inline
|
||||
func ApResult[B, R, A any](fa Result[A]) Operator[R, func(A) B, B] {
|
||||
return readert.Ap[ReaderResult[R, A], ReaderResult[R, B], ReaderResult[R, func(A) B], R, A](ET.Ap[B, error, A], FromResult[R](fa))
|
||||
}
|
||||
|
||||
// ApResultI is the curried idiomatic version of ApResult.
|
||||
// It accepts a (value, error) pair directly and applies it to a function in a ReaderResult context.
|
||||
// This bridges Go's idiomatic error handling with the functional ApResult operation.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// value, err := strconv.Atoi("10") // Returns (10, nil)
|
||||
// result := F.Pipe1(
|
||||
// readerresult.Of[Config](utils.Double),
|
||||
// readerresult.ApResultI[int, Config](value, err),
|
||||
// )
|
||||
// // result(cfg) returns result.Of(20)
|
||||
//
|
||||
//go:inline
|
||||
func ApResultI[B, R, A any](a A, err error) Operator[R, func(A) B, B] {
|
||||
return Ap[B](FromResultI[R](a, err))
|
||||
}
|
||||
|
||||
// MonadApI applies a function wrapped in a ReaderResult to a value wrapped in an idiomatic ReaderResult.
|
||||
// This is the idiomatic version of MonadAp, where the second parameter returns (A, error) instead of Result[A].
|
||||
// Both computations share the same environment.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// add := func(x int) func(int) int { return func(y int) int { return x + y } }
|
||||
// fabr := readerresult.Of[Config](add(5))
|
||||
// // Idiomatic computation returning (int, error)
|
||||
// fa := func(cfg Config) (int, error) { return cfg.Port, nil }
|
||||
// result := readerresult.MonadApI(fabr, fa)
|
||||
//
|
||||
//go:inline
|
||||
func MonadApI[B, R, A any](fab ReaderResult[R, func(A) B], fa RRI.ReaderResult[R, A]) ReaderResult[R, B] {
|
||||
return MonadAp(fab, FromReaderResultI(fa))
|
||||
}
|
||||
|
||||
// ApI is the curried version of MonadApI.
|
||||
// It allows applying to idiomatic ReaderResult values that return (A, error).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Idiomatic computation returning (int, error)
|
||||
// fa := func(cfg Config) (int, error) { return cfg.Port, nil }
|
||||
// result := F.Pipe1(fabr, readerresult.ApI[int, Config](fa))
|
||||
//
|
||||
//go:inline
|
||||
func ApI[B, R, A any](fa RRI.ReaderResult[R, A]) Operator[R, func(A) B, B] {
|
||||
return Ap[B](FromReaderResultI(fa))
|
||||
}
|
||||
|
||||
// FromPredicate creates a Kleisli arrow that tests a predicate and returns either the input value
|
||||
// or an error. If the predicate returns true, the value is returned as a success. If false,
|
||||
// the onFalse function is called to generate an error.
|
||||
@@ -266,6 +464,26 @@ func OrElse[R, A any](onLeft Kleisli[R, error, A]) Operator[R, A, A] {
|
||||
return eithert.OrElse(reader.MonadChain[R, Result[A], Result[A]], reader.Of[R, Result[A]], onLeft)
|
||||
}
|
||||
|
||||
// OrElseI provides an alternative ReaderResult computation using an idiomatic Kleisli arrow if the first one fails.
|
||||
// This is the idiomatic version of OrElse, where the fallback function returns (A, error) instead of Result[A].
|
||||
// This is useful for fallback logic or retry scenarios with idiomatic Go functions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getPrimaryUser := func(id int) readerresult.ReaderResult[DB, User] { ... }
|
||||
// // Idiomatic fallback returning (User, error)
|
||||
// getBackupUser := func(err error) func(db DB) (User, error) {
|
||||
// return func(db DB) (User, error) {
|
||||
// return User{Name: "Guest"}, nil
|
||||
// }
|
||||
// }
|
||||
// result := F.Pipe1(getPrimaryUser(42), readerresult.OrElseI[DB](getBackupUser))
|
||||
//
|
||||
//go:inline
|
||||
func OrElseI[R, A any](onLeft RRI.Kleisli[R, error, A]) Operator[R, A, A] {
|
||||
return OrElse(fromReaderResultKleisliI(onLeft))
|
||||
}
|
||||
|
||||
// OrLeft transforms the error value if the computation fails, leaving successful values unchanged.
|
||||
// This is useful for error mapping or enriching error information.
|
||||
//
|
||||
@@ -336,6 +554,24 @@ func MonadChainEitherK[R, A, B any](ma ReaderResult[R, A], f result.Kleisli[A, B
|
||||
)
|
||||
}
|
||||
|
||||
// MonadChainEitherIK chains a ReaderResult with an idiomatic function that returns (B, error).
|
||||
// This is the idiomatic version of MonadChainEitherK, accepting functions in Go's native error handling pattern.
|
||||
// The function f doesn't need environment access and returns a (value, error) pair.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getUserDataRR := readerresult.Of[DB]("user_data")
|
||||
// // Idiomatic parser returning (User, error)
|
||||
// parseUser := func(data string) (User, error) {
|
||||
// return json.Unmarshal([]byte(data), &User{})
|
||||
// }
|
||||
// result := readerresult.MonadChainEitherIK(getUserDataRR, parseUser)
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainEitherIK[R, A, B any](ma ReaderResult[R, A], f RI.Kleisli[A, B]) ReaderResult[R, B] {
|
||||
return MonadChainEitherK(ma, fromResultKleisliI(f))
|
||||
}
|
||||
|
||||
// ChainEitherK is the curried version of MonadChainEitherK.
|
||||
// It lifts a Result-returning function into a ReaderResult operator.
|
||||
//
|
||||
@@ -353,6 +589,22 @@ func ChainEitherK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, B] {
|
||||
)
|
||||
}
|
||||
|
||||
// ChainEitherIK is the curried version of MonadChainEitherIK.
|
||||
// It lifts an idiomatic function returning (B, error) into a ReaderResult operator.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Idiomatic parser returning (User, error)
|
||||
// parseUser := func(data string) (User, error) {
|
||||
// return json.Unmarshal([]byte(data), &User{})
|
||||
// }
|
||||
// result := F.Pipe1(getUserDataRR, readerresult.ChainEitherIK[DB](parseUser))
|
||||
//
|
||||
//go:inline
|
||||
func ChainEitherIK[R, A, B any](f RI.Kleisli[A, B]) Operator[R, A, B] {
|
||||
return ChainEitherK[R](fromResultKleisliI(f))
|
||||
}
|
||||
|
||||
// ChainOptionK chains with a function that returns an Option, converting None to an error.
|
||||
// This is useful for integrating functions that return optional values.
|
||||
//
|
||||
@@ -368,6 +620,32 @@ func ChainOptionK[R, A, B any](onNone Lazy[error]) func(option.Kleisli[A, B]) Op
|
||||
return fromeither.ChainOptionK(MonadChain[R, A, B], FromEither[R, B], onNone)
|
||||
}
|
||||
|
||||
// ChainOptionIK chains with an idiomatic function that returns (Option[B], error), converting None to an error.
|
||||
// This is the idiomatic version of ChainOptionK, accepting functions in Go's native error handling pattern.
|
||||
// The onNone function is called when the Option is None to generate an error.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Idiomatic function returning (Option[User], error)
|
||||
// findUser := func(id int) (option.Option[User], error) {
|
||||
// user, err := db.Query(id)
|
||||
// if err != nil {
|
||||
// return option.None[User](), err
|
||||
// }
|
||||
// if user == nil {
|
||||
// return option.None[User](), nil
|
||||
// }
|
||||
// return option.Some(*user), nil
|
||||
// }
|
||||
// notFound := func() error { return errors.New("user not found") }
|
||||
// chain := readerresult.ChainOptionIK[Config, int, User](notFound)
|
||||
// result := F.Pipe1(readerresult.Of[Config](42), chain(findUser))
|
||||
//
|
||||
//go:inline
|
||||
func ChainOptionIK[R, A, B any](onNone Lazy[error]) func(OI.Kleisli[A, B]) Operator[R, A, B] {
|
||||
return function.Flow2(fromOptionKleisliI[A, B], ChainOptionK[R, A, B](onNone))
|
||||
}
|
||||
|
||||
// Flatten removes one level of nesting from a nested ReaderResult.
|
||||
// This converts ReaderResult[R, ReaderResult[R, A]] into ReaderResult[R, A].
|
||||
//
|
||||
@@ -382,13 +660,31 @@ func Flatten[R, A any](mma ReaderResult[R, ReaderResult[R, A]]) ReaderResult[R,
|
||||
return MonadChain(mma, function.Identity[ReaderResult[R, A]])
|
||||
}
|
||||
|
||||
// FlattenI removes one level of nesting from a ReaderResult containing an idiomatic ReaderResult.
|
||||
// This converts ReaderResult[R, RRI.ReaderResult[R, A]] into ReaderResult[R, A].
|
||||
// The inner computation returns (A, error) in Go's idiomatic style.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Nested computation where inner returns (A, error)
|
||||
// nested := readerresult.Of[Config](func(cfg Config) (int, error) {
|
||||
// return 42, nil
|
||||
// })
|
||||
// flat := readerresult.FlattenI(nested)
|
||||
// // flat(cfg) returns result.Of(42)
|
||||
//
|
||||
//go:inline
|
||||
func FlattenI[R, A any](mma ReaderResult[R, RRI.ReaderResult[R, A]]) ReaderResult[R, A] {
|
||||
return MonadChain(mma, FromReaderResultI[R, A])
|
||||
}
|
||||
|
||||
// MonadBiMap maps functions over both the error and success channels simultaneously.
|
||||
// This transforms both the error type and the success type in a single operation.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// enrichErr := func(e error) error { return fmt.Errorf("enriched: %w", e) }
|
||||
// double := func(x int) int { return x * 2 }
|
||||
// double := N.Mul(2)
|
||||
// result := readerresult.MonadBiMap(rr, enrichErr, double)
|
||||
//
|
||||
//go:inline
|
||||
@@ -402,7 +698,7 @@ func MonadBiMap[R, A, B any](fa ReaderResult[R, A], f Endomorphism[error], g fun
|
||||
// Example:
|
||||
//
|
||||
// enrichErr := func(e error) error { return fmt.Errorf("enriched: %w", e) }
|
||||
// double := func(x int) int { return x * 2 }
|
||||
// double := N.Mul(2)
|
||||
// result := F.Pipe1(rr, readerresult.BiMap[Config](enrichErr, double))
|
||||
//
|
||||
//go:inline
|
||||
@@ -446,7 +742,7 @@ func Read[A, R any](r R) func(ReaderResult[R, A]) Result[A] {
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// fabr := readerresult.Of[Config](func(x int) int { return x * 2 })
|
||||
// fabr := readerresult.Of[Config](N.Mul(2))
|
||||
// result := readerresult.MonadFlap(fabr, 5) // Returns Of(10)
|
||||
//
|
||||
//go:inline
|
||||
@@ -501,6 +797,26 @@ func MonadAlt[R, A any](first ReaderResult[R, A], second Lazy[ReaderResult[R, A]
|
||||
)
|
||||
}
|
||||
|
||||
// MonadAltI tries the first computation, and if it fails, tries the second idiomatic computation.
|
||||
// This is the idiomatic version of MonadAlt, where the alternative computation returns (A, error).
|
||||
// The second computation is lazy-evaluated and only executed if the first fails.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// primary := readerresult.Left[Config, int](errors.New("primary failed"))
|
||||
// // Idiomatic alternative returning (int, error)
|
||||
// alternative := func() func(cfg Config) (int, error) {
|
||||
// return func(cfg Config) (int, error) {
|
||||
// return 42, nil
|
||||
// }
|
||||
// }
|
||||
// result := readerresult.MonadAltI(primary, alternative)
|
||||
//
|
||||
//go:inline
|
||||
func MonadAltI[R, A any](first ReaderResult[R, A], second Lazy[RRI.ReaderResult[R, A]]) ReaderResult[R, A] {
|
||||
return MonadAlt(first, function.Pipe1(second, lazy.Map(FromReaderResultI[R, A])))
|
||||
}
|
||||
|
||||
// Alt tries the first computation, and if it fails, tries the second.
|
||||
// This implements the Alternative pattern for error recovery.
|
||||
//
|
||||
@@ -513,3 +829,21 @@ func Alt[R, A any](second Lazy[ReaderResult[R, A]]) Operator[R, A, A] {
|
||||
second,
|
||||
)
|
||||
}
|
||||
|
||||
// AltI is the curried version of MonadAltI.
|
||||
// It tries the first computation, and if it fails, tries the idiomatic alternative that returns (A, error).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Idiomatic alternative returning (int, error)
|
||||
// alternative := func() func(cfg Config) (int, error) {
|
||||
// return func(cfg Config) (int, error) {
|
||||
// return 42, nil
|
||||
// }
|
||||
// }
|
||||
// result := F.Pipe1(primary, readerresult.AltI[Config](alternative))
|
||||
//
|
||||
//go:inline
|
||||
func AltI[R, A any](second Lazy[RRI.ReaderResult[R, A]]) Operator[R, A, A] {
|
||||
return Alt(function.Pipe1(second, lazy.Map(FromReaderResultI[R, A])))
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
@@ -99,7 +100,7 @@ func TestMap(t *testing.T) {
|
||||
|
||||
func TestMonadMap(t *testing.T) {
|
||||
rr := Of[MyContext](5)
|
||||
doubled := MonadMap(rr, func(x int) int { return x * 2 })
|
||||
doubled := MonadMap(rr, N.Mul(2))
|
||||
assert.Equal(t, result.Of(10), doubled(defaultContext))
|
||||
}
|
||||
|
||||
@@ -278,7 +279,7 @@ func TestFlatten(t *testing.T) {
|
||||
|
||||
func TestBiMap(t *testing.T) {
|
||||
enrichErr := func(e error) error { return fmt.Errorf("enriched: %w", e) }
|
||||
double := func(x int) int { return x * 2 }
|
||||
double := N.Mul(2)
|
||||
|
||||
res1 := F.Pipe1(Of[MyContext](5), BiMap[MyContext](enrichErr, double))(defaultContext)
|
||||
assert.Equal(t, result.Of(10), res1)
|
||||
@@ -308,7 +309,7 @@ func TestRead(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFlap(t *testing.T) {
|
||||
fabr := Of[MyContext](func(x int) int { return x * 2 })
|
||||
fabr := Of[MyContext](N.Mul(2))
|
||||
flapped := MonadFlap(fabr, 5)
|
||||
assert.Equal(t, result.Of(10), flapped(defaultContext))
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ func Of[S, R, E, A any](a A) StateReaderIOEither[S, R, E, A] {
|
||||
//
|
||||
// result := statereaderioeither.MonadMap(
|
||||
// statereaderioeither.Of[AppState, Config, error](21),
|
||||
// func(x int) int { return x * 2 },
|
||||
// N.Mul(2),
|
||||
// ) // Result contains 42
|
||||
func MonadMap[S, R, E, A, B any](fa StateReaderIOEither[S, R, E, A], f func(A) B) StateReaderIOEither[S, R, E, B] {
|
||||
return statet.MonadMap[StateReaderIOEither[S, R, E, A], StateReaderIOEither[S, R, E, B]](
|
||||
@@ -81,7 +81,7 @@ func MonadMap[S, R, E, A, B any](fa StateReaderIOEither[S, R, E, A], f func(A) B
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// double := statereaderioeither.Map[AppState, Config, error](func(x int) int { return x * 2 })
|
||||
// double := statereaderioeither.Map[AppState, Config, error](N.Mul(2))
|
||||
// result := function.Pipe1(statereaderioeither.Of[AppState, Config, error](21), double)
|
||||
func Map[S, R, E, A, B any](f func(A) B) Operator[S, R, E, A, B] {
|
||||
return statet.Map[StateReaderIOEither[S, R, E, A], StateReaderIOEither[S, R, E, B]](
|
||||
@@ -133,7 +133,7 @@ func Chain[S, R, E, A, B any](f Kleisli[S, R, E, A, B]) Operator[S, R, E, A, B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// fab := statereaderioeither.Of[AppState, Config, error](func(x int) int { return x * 2 })
|
||||
// fab := statereaderioeither.Of[AppState, Config, error](N.Mul(2))
|
||||
// fa := statereaderioeither.Of[AppState, Config, error](21)
|
||||
// result := statereaderioeither.MonadAp(fab, fa) // Result contains 42
|
||||
func MonadAp[B, S, R, E, A any](fab StateReaderIOEither[S, R, E, func(A) B], fa StateReaderIOEither[S, R, E, A]) StateReaderIOEither[S, R, E, B] {
|
||||
|
||||
@@ -252,7 +252,7 @@ func TestFromState(t *testing.T) {
|
||||
|
||||
assert.True(t, E.IsRight(res))
|
||||
E.Map[error](func(p P.Pair[testState, int]) P.Pair[testState, int] {
|
||||
assert.Equal(t, 11, P.Tail(p)) // Incremented value
|
||||
assert.Equal(t, 11, P.Tail(p)) // Incremented value
|
||||
assert.Equal(t, 11, P.Head(p).counter) // State updated
|
||||
return p
|
||||
})(res)
|
||||
@@ -568,7 +568,7 @@ func TestStatefulComputation(t *testing.T) {
|
||||
res := result(initialState)(ctx)()
|
||||
assert.True(t, E.IsRight(res))
|
||||
E.Map[error](func(p P.Pair[testState, int]) P.Pair[testState, int] {
|
||||
assert.Equal(t, 3, P.Tail(p)) // Last incremented value
|
||||
assert.Equal(t, 3, P.Tail(p)) // Last incremented value
|
||||
assert.Equal(t, 3, P.Head(p).counter) // State updated three times
|
||||
return p
|
||||
})(res)
|
||||
|
||||
Reference in New Issue
Block a user