mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-09 23:11:40 +02:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcfb023891 | ||
|
|
51cf241a26 | ||
|
|
9004c93976 | ||
|
|
d8ab6b0ce5 | ||
|
|
4e9998b645 |
174
v2/IDIOMATIC_READERIORESULT_TODO.md
Normal file
174
v2/IDIOMATIC_READERIORESULT_TODO.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# Idiomatic ReadIOResult Functions - Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the idiomatic functions that should be added to the `readerioresult` package to support Go's native `(value, error)` pattern, similar to what was implemented for `readerresult`.
|
||||
|
||||
## Key Concepts
|
||||
|
||||
The idiomatic package `github.com/IBM/fp-go/v2/idiomatic/readerioresult` defines:
|
||||
- `ReaderIOResult[R, A]` as `func(R) func() (A, error)` (idiomatic style)
|
||||
- This contrasts with `readerioresult.ReaderIOResult[R, A]` which is `Reader[R, IOResult[A]]` (functional style)
|
||||
|
||||
## Functions to Add
|
||||
|
||||
### In `readerioresult/reader.go`
|
||||
|
||||
Add helper functions at the top:
|
||||
```go
|
||||
func fromReaderIOResultKleisliI[R, A, B any](f RIORI.Kleisli[R, A, B]) Kleisli[R, A, B] {
|
||||
return function.Flow2(f, FromReaderIOResultI[R, B])
|
||||
}
|
||||
|
||||
func fromIOResultKleisliI[A, B any](f IORI.Kleisli[A, B]) ioresult.Kleisli[A, B] {
|
||||
return ioresult.Eitherize1(f)
|
||||
}
|
||||
```
|
||||
|
||||
### Core Conversion Functions
|
||||
|
||||
1. **FromResultI** - Lift `(value, error)` to ReaderIOResult
|
||||
```go
|
||||
func FromResultI[R, A any](a A, err error) ReaderIOResult[R, A]
|
||||
```
|
||||
|
||||
2. **FromIOResultI** - Lift idiomatic IOResult to functional
|
||||
```go
|
||||
func FromIOResultI[R, A any](ioe func() (A, error)) ReaderIOResult[R, A]
|
||||
```
|
||||
|
||||
3. **FromReaderIOResultI** - Convert idiomatic ReaderIOResult to functional
|
||||
```go
|
||||
func FromReaderIOResultI[R, A any](rr RIORI.ReaderIOResult[R, A]) ReaderIOResult[R, A]
|
||||
```
|
||||
|
||||
### Chain Functions
|
||||
|
||||
4. **MonadChainI** / **ChainI** - Chain with idiomatic Kleisli
|
||||
```go
|
||||
func MonadChainI[R, A, B any](ma ReaderIOResult[R, A], f RIORI.Kleisli[R, A, B]) ReaderIOResult[R, B]
|
||||
func ChainI[R, A, B any](f RIORI.Kleisli[R, A, B]) Operator[R, A, B]
|
||||
```
|
||||
|
||||
5. **MonadChainEitherIK** / **ChainEitherIK** - Chain with idiomatic Result functions
|
||||
```go
|
||||
func MonadChainEitherIK[R, A, B any](ma ReaderIOResult[R, A], f func(A) (B, error)) ReaderIOResult[R, B]
|
||||
func ChainEitherIK[R, A, B any](f func(A) (B, error)) Operator[R, A, B]
|
||||
```
|
||||
|
||||
6. **MonadChainIOResultIK** / **ChainIOResultIK** - Chain with idiomatic IOResult
|
||||
```go
|
||||
func MonadChainIOResultIK[R, A, B any](ma ReaderIOResult[R, A], f func(A) func() (B, error)) ReaderIOResult[R, B]
|
||||
func ChainIOResultIK[R, A, B any](f func(A) func() (B, error)) Operator[R, A, B]
|
||||
```
|
||||
|
||||
### Applicative Functions
|
||||
|
||||
7. **MonadApI** / **ApI** - Apply with idiomatic value
|
||||
```go
|
||||
func MonadApI[B, R, A any](fab ReaderIOResult[R, func(A) B], fa RIORI.ReaderIOResult[R, A]) ReaderIOResult[R, B]
|
||||
func ApI[B, R, A any](fa RIORI.ReaderIOResult[R, A]) Operator[R, func(A) B, B]
|
||||
```
|
||||
|
||||
### Error Handling Functions
|
||||
|
||||
8. **OrElseI** - Fallback with idiomatic computation
|
||||
```go
|
||||
func OrElseI[R, A any](onLeft RIORI.Kleisli[R, error, A]) Operator[R, A, A]
|
||||
```
|
||||
|
||||
9. **MonadAltI** / **AltI** - Alternative with idiomatic computation
|
||||
```go
|
||||
func MonadAltI[R, A any](first ReaderIOResult[R, A], second Lazy[RIORI.ReaderIOResult[R, A]]) ReaderIOResult[R, A]
|
||||
func AltI[R, A any](second Lazy[RIORI.ReaderIOResult[R, A]]) Operator[R, A, A]
|
||||
```
|
||||
|
||||
### Flatten Functions
|
||||
|
||||
10. **FlattenI** - Flatten nested idiomatic ReaderIOResult
|
||||
```go
|
||||
func FlattenI[R, A any](mma ReaderIOResult[R, RIORI.ReaderIOResult[R, A]]) ReaderIOResult[R, A]
|
||||
```
|
||||
|
||||
### In `readerioresult/bind.go`
|
||||
|
||||
11. **BindI** - Bind with idiomatic Kleisli
|
||||
```go
|
||||
func BindI[R, S1, S2, T any](setter func(T) func(S1) S2, f RIORI.Kleisli[R, S1, T]) Operator[R, S1, S2]
|
||||
```
|
||||
|
||||
12. **ApIS** - Apply idiomatic value to state
|
||||
```go
|
||||
func ApIS[R, S1, S2, T any](setter func(T) func(S1) S2, fa RIORI.ReaderIOResult[R, T]) Operator[R, S1, S2]
|
||||
```
|
||||
|
||||
13. **ApISL** - Apply idiomatic value using lens
|
||||
```go
|
||||
func ApISL[R, S, T any](lens L.Lens[S, T], fa RIORI.ReaderIOResult[R, T]) Operator[R, S, S]
|
||||
```
|
||||
|
||||
14. **BindIL** - Bind idiomatic with lens
|
||||
```go
|
||||
func BindIL[R, S, T any](lens L.Lens[S, T], f RIORI.Kleisli[R, T, T]) Operator[R, S, S]
|
||||
```
|
||||
|
||||
15. **BindEitherIK** / **BindResultIK** - Bind idiomatic Result
|
||||
```go
|
||||
func BindEitherIK[R, S1, S2, T any](setter func(T) func(S1) S2, f func(S1) (T, error)) Operator[R, S1, S2]
|
||||
func BindResultIK[R, S1, S2, T any](setter func(T) func(S1) S2, f func(S1) (T, error)) Operator[R, S1, S2]
|
||||
```
|
||||
|
||||
16. **BindIOResultIK** - Bind idiomatic IOResult
|
||||
```go
|
||||
func BindIOResultIK[R, S1, S2, T any](setter func(T) func(S1) S2, f func(S1) func() (T, error)) Operator[R, S1, S2]
|
||||
```
|
||||
|
||||
17. **BindToEitherI** / **BindToResultI** - Initialize from idiomatic pair
|
||||
```go
|
||||
func BindToEitherI[R, S1, T any](setter func(T) S1) func(T, error) ReaderIOResult[R, S1]
|
||||
func BindToResultI[R, S1, T any](setter func(T) S1) func(T, error) ReaderIOResult[R, S1]
|
||||
```
|
||||
|
||||
18. **BindToIOResultI** - Initialize from idiomatic IOResult
|
||||
```go
|
||||
func BindToIOResultI[R, S1, T any](setter func(T) S1) func(func() (T, error)) ReaderIOResult[R, S1]
|
||||
```
|
||||
|
||||
19. **ApEitherIS** / **ApResultIS** - Apply idiomatic pair to state
|
||||
```go
|
||||
func ApEitherIS[R, S1, S2, T any](setter func(T) func(S1) S2) func(T, error) Operator[R, S1, S2]
|
||||
func ApResultIS[R, S1, S2, T any](setter func(T) func(S1) S2) func(T, error) Operator[R, S1, S2]
|
||||
```
|
||||
|
||||
20. **ApIOResultIS** - Apply idiomatic IOResult to state
|
||||
```go
|
||||
func ApIOResultIS[R, S1, S2, T any](setter func(T) func(S1) S2, fa func() (T, error)) Operator[R, S1, S2]
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Create `readerioresult/idiomatic_test.go` with:
|
||||
- Tests for each idiomatic function
|
||||
- Success and error cases
|
||||
- Integration tests showing real-world usage patterns
|
||||
- Parallel execution tests where applicable
|
||||
- Complex scenarios combining multiple idiomatic functions
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
1. **High Priority** - Core conversion and chain functions (1-6)
|
||||
2. **Medium Priority** - Bind functions for do-notation (11-16)
|
||||
3. **Low Priority** - Advanced applicative and error handling (7-10, 17-20)
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Seamless Integration** - Mix Go idiomatic code with functional pipelines
|
||||
2. **Gradual Adoption** - Convert code incrementally from idiomatic to functional
|
||||
3. **Interoperability** - Work with existing Go libraries that return `(value, error)`
|
||||
4. **Consistency** - Mirrors the successful pattern from `readerresult`
|
||||
|
||||
## References
|
||||
|
||||
- See `readerresult` package for similar implementations
|
||||
- See `idiomatic/readerresult` for the idiomatic types
|
||||
- See `idiomatic/ioresult` for IO-level idiomatic patterns
|
||||
268
v2/assert/assert.go
Normal file
268
v2/assert/assert.go
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
44
v2/idiomatic/readerioresult/types.go
Normal file
44
v2/idiomatic/readerioresult/types.go
Normal 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]
|
||||
)
|
||||
283
v2/idiomatic/readerresult/benchmark_test.go
Normal file
283
v2/idiomatic/readerresult/benchmark_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -17,6 +17,8 @@ package reader
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
RA "github.com/IBM/fp-go/v2/internal/array"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
G "github.com/IBM/fp-go/v2/reader/generic"
|
||||
)
|
||||
|
||||
@@ -100,3 +102,273 @@ func TraverseArrayWithIndex[R, A, B any](f func(int, A) Reader[R, B]) func([]A)
|
||||
func SequenceArray[R, A any](ma []Reader[R, A]) Reader[R, []A] {
|
||||
return MonadTraverseArray(ma, function.Identity[Reader[R, A]])
|
||||
}
|
||||
|
||||
// MonadReduceArray reduces an array of Readers to a single Reader by applying a reduction function.
|
||||
// This is the monadic version that takes the array of Readers as the first parameter.
|
||||
//
|
||||
// Each Reader is evaluated with the same environment R, and the results are accumulated using
|
||||
// the provided reduce function starting from the initial value.
|
||||
//
|
||||
// Parameters:
|
||||
// - as: Array of Readers to reduce
|
||||
// - reduce: Binary function that combines accumulated value with each Reader's result
|
||||
// - initial: Starting value for the reduction
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Base int }
|
||||
// readers := []reader.Reader[Config, int]{
|
||||
// reader.Asks(func(c Config) int { return c.Base + 1 }),
|
||||
// reader.Asks(func(c Config) int { return c.Base + 2 }),
|
||||
// reader.Asks(func(c Config) int { return c.Base + 3 }),
|
||||
// }
|
||||
// sum := func(acc, val int) int { return acc + val }
|
||||
// r := reader.MonadReduceArray(readers, sum, 0)
|
||||
// result := r(Config{Base: 10}) // 36 (11 + 12 + 13)
|
||||
//
|
||||
//go:inline
|
||||
func MonadReduceArray[R, A, B any](as []Reader[R, A], reduce func(B, A) B, initial B) Reader[R, B] {
|
||||
return RA.MonadTraverseReduce(
|
||||
Of,
|
||||
Map,
|
||||
Ap,
|
||||
|
||||
as,
|
||||
|
||||
function.Identity[Reader[R, A]],
|
||||
reduce,
|
||||
initial,
|
||||
)
|
||||
}
|
||||
|
||||
// ReduceArray returns a curried function that reduces an array of Readers to a single Reader.
|
||||
// This is the curried version where the reduction function and initial value are provided first,
|
||||
// returning a function that takes the array of Readers.
|
||||
//
|
||||
// Parameters:
|
||||
// - reduce: Binary function that combines accumulated value with each Reader's result
|
||||
// - initial: Starting value for the reduction
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an array of Readers and returns a Reader of the reduced result
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Multiplier int }
|
||||
// product := func(acc, val int) int { return acc * val }
|
||||
// reducer := reader.ReduceArray[Config](product, 1)
|
||||
// readers := []reader.Reader[Config, int]{
|
||||
// reader.Asks(func(c Config) int { return c.Multiplier * 2 }),
|
||||
// reader.Asks(func(c Config) int { return c.Multiplier * 3 }),
|
||||
// }
|
||||
// r := reducer(readers)
|
||||
// result := r(Config{Multiplier: 5}) // 150 (10 * 15)
|
||||
//
|
||||
//go:inline
|
||||
func ReduceArray[R, A, B any](reduce func(B, A) B, initial B) Kleisli[R, []Reader[R, A], B] {
|
||||
return RA.TraverseReduce[[]Reader[R, A]](
|
||||
Of,
|
||||
Map,
|
||||
Ap,
|
||||
|
||||
function.Identity[Reader[R, A]],
|
||||
reduce,
|
||||
initial,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadReduceArrayM reduces an array of Readers using a Monoid to combine the results.
|
||||
// This is the monadic version that takes the array of Readers as the first parameter.
|
||||
//
|
||||
// The Monoid provides both the binary operation (Concat) and the identity element (Empty)
|
||||
// for the reduction, making it convenient when working with monoidal types.
|
||||
//
|
||||
// Parameters:
|
||||
// - as: Array of Readers to reduce
|
||||
// - m: Monoid that defines how to combine the Reader results
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Factor int }
|
||||
// readers := []reader.Reader[Config, int]{
|
||||
// reader.Asks(func(c Config) int { return c.Factor }),
|
||||
// reader.Asks(func(c Config) int { return c.Factor * 2 }),
|
||||
// reader.Asks(func(c Config) int { return c.Factor * 3 }),
|
||||
// }
|
||||
// intAddMonoid := monoid.MakeMonoid(func(a, b int) int { return a + b }, 0)
|
||||
// r := reader.MonadReduceArrayM(readers, intAddMonoid)
|
||||
// result := r(Config{Factor: 5}) // 30 (5 + 10 + 15)
|
||||
//
|
||||
//go:inline
|
||||
func MonadReduceArrayM[R, A any](as []Reader[R, A], m monoid.Monoid[A]) Reader[R, A] {
|
||||
return MonadReduceArray(as, m.Concat, m.Empty())
|
||||
}
|
||||
|
||||
// ReduceArrayM returns a curried function that reduces an array of Readers using a Monoid.
|
||||
// This is the curried version where the Monoid is provided first, returning a function
|
||||
// that takes the array of Readers.
|
||||
//
|
||||
// The Monoid provides both the binary operation (Concat) and the identity element (Empty)
|
||||
// for the reduction.
|
||||
//
|
||||
// Parameters:
|
||||
// - m: Monoid that defines how to combine the Reader results
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an array of Readers and returns a Reader of the reduced result
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Scale int }
|
||||
// intMultMonoid := monoid.MakeMonoid(func(a, b int) int { return a * b }, 1)
|
||||
// reducer := reader.ReduceArrayM[Config](intMultMonoid)
|
||||
// readers := []reader.Reader[Config, int]{
|
||||
// reader.Asks(func(c Config) int { return c.Scale }),
|
||||
// reader.Asks(func(c Config) int { return c.Scale * 2 }),
|
||||
// }
|
||||
// r := reducer(readers)
|
||||
// result := r(Config{Scale: 3}) // 18 (3 * 6)
|
||||
//
|
||||
//go:inline
|
||||
func ReduceArrayM[R, A any](m monoid.Monoid[A]) Kleisli[R, []Reader[R, A], A] {
|
||||
return ReduceArray[R](m.Concat, m.Empty())
|
||||
}
|
||||
|
||||
// MonadTraverseReduceArray transforms and reduces an array in one operation.
|
||||
// This is the monadic version that takes the array as the first parameter.
|
||||
//
|
||||
// First, each element is transformed using the provided Kleisli function into a Reader.
|
||||
// Then, the Reader results are reduced using the provided reduction function.
|
||||
//
|
||||
// This is more efficient than calling TraverseArray followed by a separate reduce operation,
|
||||
// as it combines both operations into a single traversal.
|
||||
//
|
||||
// Parameters:
|
||||
// - as: Array of elements to transform and reduce
|
||||
// - trfrm: Function that transforms each element into a Reader
|
||||
// - reduce: Binary function that combines accumulated value with each transformed result
|
||||
// - initial: Starting value for the reduction
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Multiplier int }
|
||||
// numbers := []int{1, 2, 3, 4}
|
||||
// multiply := func(n int) reader.Reader[Config, int] {
|
||||
// return reader.Asks(func(c Config) int { return n * c.Multiplier })
|
||||
// }
|
||||
// sum := func(acc, val int) int { return acc + val }
|
||||
// r := reader.MonadTraverseReduceArray(numbers, multiply, sum, 0)
|
||||
// result := r(Config{Multiplier: 10}) // 100 (10 + 20 + 30 + 40)
|
||||
//
|
||||
//go:inline
|
||||
func MonadTraverseReduceArray[R, A, B, C any](as []A, trfrm Kleisli[R, A, B], reduce func(C, B) C, initial C) Reader[R, C] {
|
||||
return RA.MonadTraverseReduce(
|
||||
Of,
|
||||
Map,
|
||||
Ap,
|
||||
|
||||
as,
|
||||
|
||||
trfrm,
|
||||
reduce,
|
||||
initial,
|
||||
)
|
||||
}
|
||||
|
||||
// TraverseReduceArray returns a curried function that transforms and reduces an array.
|
||||
// This is the curried version where the transformation function, reduce function, and initial value
|
||||
// are provided first, returning a function that takes the array.
|
||||
//
|
||||
// First, each element is transformed using the provided Kleisli function into a Reader.
|
||||
// Then, the Reader results are reduced using the provided reduction function.
|
||||
//
|
||||
// Parameters:
|
||||
// - trfrm: Function that transforms each element into a Reader
|
||||
// - reduce: Binary function that combines accumulated value with each transformed result
|
||||
// - initial: Starting value for the reduction
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an array and returns a Reader of the reduced result
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Base int }
|
||||
// addBase := func(n int) reader.Reader[Config, int] {
|
||||
// return reader.Asks(func(c Config) int { return n + c.Base })
|
||||
// }
|
||||
// product := func(acc, val int) int { return acc * val }
|
||||
// transformer := reader.TraverseReduceArray(addBase, product, 1)
|
||||
// r := transformer([]int{2, 3, 4})
|
||||
// result := r(Config{Base: 10}) // 2184 (12 * 13 * 14)
|
||||
//
|
||||
//go:inline
|
||||
func TraverseReduceArray[R, A, B, C any](trfrm Kleisli[R, A, B], reduce func(C, B) C, initial C) Kleisli[R, []A, C] {
|
||||
return RA.TraverseReduce[[]A](
|
||||
Of,
|
||||
Map,
|
||||
Ap,
|
||||
|
||||
trfrm,
|
||||
reduce,
|
||||
initial,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadTraverseReduceArrayM transforms and reduces an array using a Monoid.
|
||||
// This is the monadic version that takes the array as the first parameter.
|
||||
//
|
||||
// First, each element is transformed using the provided Kleisli function into a Reader.
|
||||
// Then, the Reader results are reduced using the Monoid's binary operation and identity element.
|
||||
//
|
||||
// This combines transformation and monoidal reduction in a single efficient operation.
|
||||
//
|
||||
// Parameters:
|
||||
// - as: Array of elements to transform and reduce
|
||||
// - trfrm: Function that transforms each element into a Reader
|
||||
// - m: Monoid that defines how to combine the transformed results
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Offset int }
|
||||
// numbers := []int{1, 2, 3}
|
||||
// addOffset := func(n int) reader.Reader[Config, int] {
|
||||
// return reader.Asks(func(c Config) int { return n + c.Offset })
|
||||
// }
|
||||
// intSumMonoid := monoid.MakeMonoid(func(a, b int) int { return a + b }, 0)
|
||||
// r := reader.MonadTraverseReduceArrayM(numbers, addOffset, intSumMonoid)
|
||||
// result := r(Config{Offset: 100}) // 306 (101 + 102 + 103)
|
||||
//
|
||||
//go:inline
|
||||
func MonadTraverseReduceArrayM[R, A, B any](as []A, trfrm Kleisli[R, A, B], m monoid.Monoid[B]) Reader[R, B] {
|
||||
return MonadTraverseReduceArray(as, trfrm, m.Concat, m.Empty())
|
||||
}
|
||||
|
||||
// TraverseReduceArrayM returns a curried function that transforms and reduces an array using a Monoid.
|
||||
// This is the curried version where the transformation function and Monoid are provided first,
|
||||
// returning a function that takes the array.
|
||||
//
|
||||
// First, each element is transformed using the provided Kleisli function into a Reader.
|
||||
// Then, the Reader results are reduced using the Monoid's binary operation and identity element.
|
||||
//
|
||||
// Parameters:
|
||||
// - trfrm: Function that transforms each element into a Reader
|
||||
// - m: Monoid that defines how to combine the transformed results
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an array and returns a Reader of the reduced result
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Factor int }
|
||||
// scale := func(n int) reader.Reader[Config, int] {
|
||||
// return reader.Asks(func(c Config) int { return n * c.Factor })
|
||||
// }
|
||||
// intProdMonoid := monoid.MakeMonoid(func(a, b int) int { return a * b }, 1)
|
||||
// transformer := reader.TraverseReduceArrayM(scale, intProdMonoid)
|
||||
// r := transformer([]int{2, 3, 4})
|
||||
// result := r(Config{Factor: 5}) // 3000 (10 * 15 * 20)
|
||||
//
|
||||
//go:inline
|
||||
func TraverseReduceArrayM[R, A, B any](trfrm Kleisli[R, A, B], m monoid.Monoid[B]) Kleisli[R, []A, B] {
|
||||
return TraverseReduceArray(trfrm, m.Concat, m.Empty())
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -93,3 +94,142 @@ func TestMonadTraverseArray(t *testing.T) {
|
||||
assert.Equal(t, 3, len(result))
|
||||
assert.Contains(t, result[0], "num")
|
||||
}
|
||||
|
||||
func TestMonadReduceArray(t *testing.T) {
|
||||
type Config struct{ Base int }
|
||||
config := Config{Base: 10}
|
||||
|
||||
readers := []Reader[Config, int]{
|
||||
Asks(func(c Config) int { return c.Base + 1 }),
|
||||
Asks(func(c Config) int { return c.Base + 2 }),
|
||||
Asks(func(c Config) int { return c.Base + 3 }),
|
||||
}
|
||||
|
||||
sum := func(acc, val int) int { return acc + val }
|
||||
r := MonadReduceArray(readers, sum, 0)
|
||||
result := r(config)
|
||||
|
||||
assert.Equal(t, 36, result) // 11 + 12 + 13
|
||||
}
|
||||
|
||||
func TestReduceArray(t *testing.T) {
|
||||
type Config struct{ Multiplier int }
|
||||
config := Config{Multiplier: 5}
|
||||
|
||||
product := func(acc, val int) int { return acc * val }
|
||||
reducer := ReduceArray[Config](product, 1)
|
||||
|
||||
readers := []Reader[Config, int]{
|
||||
Asks(func(c Config) int { return c.Multiplier * 2 }),
|
||||
Asks(func(c Config) int { return c.Multiplier * 3 }),
|
||||
}
|
||||
|
||||
r := reducer(readers)
|
||||
result := r(config)
|
||||
|
||||
assert.Equal(t, 150, result) // 10 * 15
|
||||
}
|
||||
|
||||
func TestMonadReduceArrayM(t *testing.T) {
|
||||
type Config struct{ Factor int }
|
||||
config := Config{Factor: 5}
|
||||
|
||||
readers := []Reader[Config, int]{
|
||||
Asks(func(c Config) int { return c.Factor }),
|
||||
Asks(func(c Config) int { return c.Factor * 2 }),
|
||||
Asks(func(c Config) int { return c.Factor * 3 }),
|
||||
}
|
||||
|
||||
intAddMonoid := M.MakeMonoid(func(a, b int) int { return a + b }, 0)
|
||||
|
||||
r := MonadReduceArrayM(readers, intAddMonoid)
|
||||
result := r(config)
|
||||
|
||||
assert.Equal(t, 30, result) // 5 + 10 + 15
|
||||
}
|
||||
|
||||
func TestReduceArrayM(t *testing.T) {
|
||||
type Config struct{ Scale int }
|
||||
config := Config{Scale: 3}
|
||||
|
||||
intMultMonoid := M.MakeMonoid(func(a, b int) int { return a * b }, 1)
|
||||
|
||||
reducer := ReduceArrayM[Config](intMultMonoid)
|
||||
|
||||
readers := []Reader[Config, int]{
|
||||
Asks(func(c Config) int { return c.Scale }),
|
||||
Asks(func(c Config) int { return c.Scale * 2 }),
|
||||
}
|
||||
|
||||
r := reducer(readers)
|
||||
result := r(config)
|
||||
|
||||
assert.Equal(t, 18, result) // 3 * 6
|
||||
}
|
||||
|
||||
func TestMonadTraverseReduceArray(t *testing.T) {
|
||||
type Config struct{ Multiplier int }
|
||||
config := Config{Multiplier: 10}
|
||||
|
||||
numbers := []int{1, 2, 3, 4}
|
||||
multiply := func(n int) Reader[Config, int] {
|
||||
return Asks(func(c Config) int { return n * c.Multiplier })
|
||||
}
|
||||
|
||||
sum := func(acc, val int) int { return acc + val }
|
||||
r := MonadTraverseReduceArray(numbers, multiply, sum, 0)
|
||||
result := r(config)
|
||||
|
||||
assert.Equal(t, 100, result) // 10 + 20 + 30 + 40
|
||||
}
|
||||
|
||||
func TestTraverseReduceArray(t *testing.T) {
|
||||
type Config struct{ Base int }
|
||||
config := Config{Base: 10}
|
||||
|
||||
addBase := func(n int) Reader[Config, int] {
|
||||
return Asks(func(c Config) int { return n + c.Base })
|
||||
}
|
||||
|
||||
product := func(acc, val int) int { return acc * val }
|
||||
transformer := TraverseReduceArray(addBase, product, 1)
|
||||
|
||||
r := transformer([]int{2, 3, 4})
|
||||
result := r(config)
|
||||
|
||||
assert.Equal(t, 2184, result) // 12 * 13 * 14
|
||||
}
|
||||
|
||||
func TestMonadTraverseReduceArrayM(t *testing.T) {
|
||||
type Config struct{ Offset int }
|
||||
config := Config{Offset: 100}
|
||||
|
||||
numbers := []int{1, 2, 3}
|
||||
addOffset := func(n int) Reader[Config, int] {
|
||||
return Asks(func(c Config) int { return n + c.Offset })
|
||||
}
|
||||
|
||||
intSumMonoid := M.MakeMonoid(func(a, b int) int { return a + b }, 0)
|
||||
|
||||
r := MonadTraverseReduceArrayM(numbers, addOffset, intSumMonoid)
|
||||
result := r(config)
|
||||
|
||||
assert.Equal(t, 306, result) // 101 + 102 + 103
|
||||
}
|
||||
|
||||
func TestTraverseReduceArrayM(t *testing.T) {
|
||||
type Config struct{ Factor int }
|
||||
config := Config{Factor: 5}
|
||||
|
||||
scale := func(n int) Reader[Config, int] {
|
||||
return Asks(func(c Config) int { return n * c.Factor })
|
||||
}
|
||||
|
||||
intProdMonoid := M.MakeMonoid(func(a, b int) int { return a * b }, 1)
|
||||
|
||||
transformer := TraverseReduceArrayM(scale, intProdMonoid)
|
||||
r := transformer([]int{2, 3, 4})
|
||||
result := r(config)
|
||||
|
||||
assert.Equal(t, 3000, result) // 10 * 15 * 20
|
||||
}
|
||||
|
||||
68
v2/reader/record.go
Normal file
68
v2/reader/record.go
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package reader
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
RR "github.com/IBM/fp-go/v2/internal/record"
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func MonadTraverseRecord[K comparable, R, A, B any](ma map[K]A, f Kleisli[R, A, B]) Reader[R, map[K]B] {
|
||||
return RR.MonadTraverse[map[K]A, map[K]B](
|
||||
Of,
|
||||
Map,
|
||||
Ap,
|
||||
ma,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TraverseRecord[K comparable, R, A, B any](f Kleisli[R, A, B]) func(map[K]A) Reader[R, map[K]B] {
|
||||
return RR.Traverse[map[K]A, map[K]B](
|
||||
Of,
|
||||
Map,
|
||||
Ap,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadTraverseRecordWithIndex[K comparable, R, A, B any](ma map[K]A, f func(K, A) Reader[R, B]) Reader[R, map[K]B] {
|
||||
return RR.MonadTraverseWithIndex[map[K]A, map[K]B](
|
||||
Of,
|
||||
Map,
|
||||
Ap,
|
||||
ma,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TraverseRecordWithIndex[K comparable, R, A, B any](f func(K, A) Reader[R, B]) func(map[K]A) Reader[R, map[K]B] {
|
||||
return RR.TraverseWithIndex[map[K]A, map[K]B](
|
||||
Of,
|
||||
Map,
|
||||
Ap,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func SequenceRecord[K comparable, R, A any](ma map[K]Reader[R, A]) Reader[R, map[K]A] {
|
||||
return MonadTraverseRecord(ma, function.Identity[Reader[R, A]])
|
||||
}
|
||||
@@ -66,6 +66,14 @@ func Chain[E, L, A, B any](f func(A) ReaderEither[E, L, B]) func(ReaderEither[E,
|
||||
return readert.Chain[ReaderEither[E, L, A]](ET.Chain[L, A, B], f)
|
||||
}
|
||||
|
||||
func MonadChainReaderK[E, L, A, B any](ma ReaderEither[E, L, A], f reader.Kleisli[E, A, B]) ReaderEither[E, L, B] {
|
||||
return MonadChain(ma, function.Flow2(f, FromReader[E, L, B]))
|
||||
}
|
||||
|
||||
func ChainReaderK[E, L, A, B any](f reader.Kleisli[E, A, B]) func(ReaderEither[E, L, A]) ReaderEither[E, L, B] {
|
||||
return Chain(function.Flow2(f, FromReader[E, L, B]))
|
||||
}
|
||||
|
||||
func Of[E, L, A any](a A) ReaderEither[E, L, A] {
|
||||
return readert.MonadOf[ReaderEither[E, L, A]](ET.Of[L, A], a)
|
||||
}
|
||||
|
||||
286
v2/readerresult/benchmark_test.go
Normal file
286
v2/readerresult/benchmark_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
1091
v2/readerresult/idiomatic_test.go
Normal file
1091
v2/readerresult/idiomatic_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -18,16 +18,32 @@ package readerresult
|
||||
import (
|
||||
ET "github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
OI "github.com/IBM/fp-go/v2/idiomatic/option"
|
||||
RRI "github.com/IBM/fp-go/v2/idiomatic/readerresult"
|
||||
RI "github.com/IBM/fp-go/v2/idiomatic/result"
|
||||
"github.com/IBM/fp-go/v2/internal/eithert"
|
||||
"github.com/IBM/fp-go/v2/internal/fromeither"
|
||||
"github.com/IBM/fp-go/v2/internal/fromreader"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/readert"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
func fromReaderResultKleisliI[R, A, B any](f RRI.Kleisli[R, A, B]) Kleisli[R, A, B] {
|
||||
return function.Flow2(f, FromReaderResultI[R, B])
|
||||
}
|
||||
|
||||
func fromResultKleisliI[A, B any](f RI.Kleisli[A, B]) result.Kleisli[A, B] {
|
||||
return result.Eitherize1(f)
|
||||
}
|
||||
|
||||
func fromOptionKleisliI[A, B any](f OI.Kleisli[A, B]) option.Kleisli[A, B] {
|
||||
return option.Optionize1(f)
|
||||
}
|
||||
|
||||
// FromEither lifts a Result[A] into a ReaderResult[R, A] that ignores the environment.
|
||||
// The resulting computation will always produce the same result regardless of the environment provided.
|
||||
//
|
||||
@@ -50,6 +66,46 @@ func FromResult[R, A any](e Result[A]) ReaderResult[R, A] {
|
||||
return reader.Of[R](e)
|
||||
}
|
||||
|
||||
// FromResultI lifts an idiomatic Go (value, error) pair into a ReaderResult[R, A] that ignores the environment.
|
||||
// This is the idiomatic version of FromResult, accepting Go's native error handling pattern.
|
||||
// If err is non-nil, the resulting computation will always fail with that error.
|
||||
// If err is nil, the resulting computation will always succeed with the value a.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// value, err := strconv.Atoi("42")
|
||||
// rr := readerresult.FromResultI[Config](value, err)
|
||||
// // rr(anyConfig) will return result.Of(42) if err is nil
|
||||
//
|
||||
//go:inline
|
||||
func FromResultI[R, A any](a A, err error) ReaderResult[R, A] {
|
||||
return reader.Of[R](result.TryCatchError(a, err))
|
||||
}
|
||||
|
||||
// FromReaderResultI converts an idiomatic ReaderResult (that returns (A, error)) into a functional ReaderResult (that returns Result[A]).
|
||||
// This bridges the gap between Go's idiomatic error handling and functional programming style.
|
||||
// The idiomatic RRI.ReaderResult[R, A] is a function R -> (A, error).
|
||||
// The functional ReaderResult[R, A] is a function R -> Result[A].
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Idiomatic ReaderResult
|
||||
// getUserID := func(cfg Config) (int, error) {
|
||||
// if cfg.Valid {
|
||||
// return 42, nil
|
||||
// }
|
||||
// return 0, errors.New("invalid config")
|
||||
// }
|
||||
// rr := readerresult.FromReaderResultI(getUserID)
|
||||
// // rr is now a functional ReaderResult[Config, int]
|
||||
//
|
||||
//go:inline
|
||||
func FromReaderResultI[R, A any](rr RRI.ReaderResult[R, A]) ReaderResult[R, A] {
|
||||
return func(r R) Result[A] {
|
||||
return result.TryCatchError(rr(r))
|
||||
}
|
||||
}
|
||||
|
||||
// RightReader lifts a Reader[R, A] into a ReaderResult[R, A] that always succeeds.
|
||||
// The resulting computation reads a value from the environment and wraps it in a successful Result.
|
||||
//
|
||||
@@ -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])))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user