1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-12-09 23:11:40 +02:00

Compare commits

...

5 Commits

Author SHA1 Message Date
Dr. Carsten Leue
dcfb023891 fix: improve assertions
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-24 17:28:48 +01:00
Dr. Carsten Leue
51cf241a26 fix: add ReaderK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-24 12:29:55 +01:00
Dr. Carsten Leue
9004c93976 fix: add some idomatic helpers
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-24 10:40:58 +01:00
Dr. Carsten Leue
d8ab6b0ce5 fix: ChainReaderK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-22 10:39:56 +01:00
Dr. Carsten Leue
4e9998b645 fix: benchmarks and better docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-21 15:39:41 +01:00
19 changed files with 3887 additions and 81 deletions

View 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

268
v2/assert/assert.go Normal file
View File

@@ -0,0 +1,268 @@
// 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/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)
}
}
// 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
}
}

View File

@@ -16,94 +16,470 @@
package assert
import (
"fmt"
"errors"
"testing"
"github.com/IBM/fp-go/v2/eq"
"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[int](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[int](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)
func TestIntegration(t *testing.T) {
t.Run("complex assertion composition", func(t *testing.T) {
type User struct {
Name string
Age int
Email string
}
return result.Left[map[K]T](errTest)
}
user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
assertions := AllOf([]Reader{
Equal("Alice")(user.Name),
Equal(30)(user.Age),
That(func(s string) bool { return len(s) > 0 })(user.Email),
})
result := assertions(t)
if !result {
t.Error("Expected complex assertion composition to pass")
}
})
t.Run("test suite with RunAll", func(t *testing.T) {
data := []int{1, 2, 3, 4, 5}
suite := RunAll(map[string]Reader{
"not_empty": ArrayNotEmpty(data),
"correct_size": ArrayLength[int](5)(data),
"contains_one": ArrayContains(1)(data),
"contains_five": ArrayContains(5)(data),
})
result := suite(t)
if !result {
t.Error("Expected test suite to pass")
}
})
}

View File

@@ -1,7 +1,16 @@
package assert
import "github.com/IBM/fp-go/v2/result"
import (
"testing"
"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]
)

View File

@@ -0,0 +1,44 @@
// 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[A any] = endomorphism.Endomorphism[A]
Lazy[A any] = lazy.Lazy[A]
Option[A any] = option.Option[A]
Result[A any] = result.Result[A]
Reader[R, A any] = reader.Reader[R, A]
IO[A any] = io.IO[A]
IOResult[A any] = ioresult.IOResult[A]
ReaderIOResult[R, A any] = Reader[R, IOResult[A]]
Monoid[R, A any] = monoid.Monoid[ReaderIOResult[R, A]]
Kleisli[R, A, B any] = Reader[A, ReaderIOResult[R, B]]
Operator[R, A, B any] = Kleisli[R, ReaderIOResult[R, A], B]
)

View File

@@ -0,0 +1,283 @@
// 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"
)
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 := func(x int) int { return x * 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 := func(x int) int { return x * 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](func(x int) int { return x * 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 := func(x int) int { return x * 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)
}
}

View File

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

View File

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

View File

@@ -24,6 +24,7 @@ import (
"github.com/IBM/fp-go/v2/internal/utils"
"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"
)
@@ -271,7 +272,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 +280,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)

View File

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

View File

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

View File

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

View File

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

68
v2/reader/record.go Normal file
View 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]])
}

View File

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

View File

@@ -0,0 +1,286 @@
// 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"
)
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 := func(x int) int { return x * 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 := func(x int) int { return x * 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](func(x int) int { return x * 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 := func(x int) int { return x * 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)
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -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.
//
@@ -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,6 +660,24 @@ 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.
//
@@ -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])))
}