mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-09 23:11:40 +02:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1e8d397c3 | ||
|
|
dbe7102e43 | ||
|
|
09aeb996e2 | ||
|
|
7cd575d95a | ||
|
|
dcfb023891 | ||
|
|
51cf241a26 | ||
|
|
9004c93976 | ||
|
|
d8ab6b0ce5 | ||
|
|
4e9998b645 | ||
|
|
2ea9e292e1 | ||
|
|
12a20e30d1 | ||
|
|
4909ad5473 | ||
|
|
d116317cde | ||
|
|
1428241f2c | ||
|
|
ef9216bad7 | ||
|
|
fe77c770b6 | ||
|
|
1c42b2ac1d |
@@ -4,7 +4,12 @@
|
||||
"Bash(ls -la \"c:\\d\\fp-go\\v2\\internal\\monad\"\" && ls -la \"c:dfp-gov2internalapplicative\"\")",
|
||||
"Bash(ls -la \"c:\\d\\fp-go\\v2\\internal\\chain\"\" && ls -la \"c:dfp-gov2internalfunctor\"\")",
|
||||
"Bash(go build:*)",
|
||||
"Bash(go test:*)"
|
||||
"Bash(go test:*)",
|
||||
"Bash(go doc:*)",
|
||||
"Bash(go tool cover:*)",
|
||||
"Bash(sort:*)",
|
||||
"Bash(tee:*)",
|
||||
"Bash(find:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@@ -314,7 +314,7 @@ if err != nil {
|
||||
|
||||
```go
|
||||
// Map transforms the success value
|
||||
double := result.Map(func(x int) int { return x * 2 })
|
||||
double := result.Map(N.Mul(2))
|
||||
result := double(result.Right[error](21)) // Right(42)
|
||||
|
||||
// Chain sequences operations
|
||||
@@ -330,7 +330,7 @@ validate := result.Chain(func(x int) result.Result[int] {
|
||||
|
||||
```go
|
||||
// Map transforms the success value
|
||||
double := result.Map(func(x int) int { return x * 2 })
|
||||
double := result.Map(N.Mul(2))
|
||||
value, err := double(21, nil) // (42, nil)
|
||||
|
||||
// Chain sequences operations
|
||||
|
||||
174
v2/IDIOMATIC_READERIORESULT_TODO.md
Normal file
174
v2/IDIOMATIC_READERIORESULT_TODO.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# Idiomatic ReadIOResult Functions - Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the idiomatic functions that should be added to the `readerioresult` package to support Go's native `(value, error)` pattern, similar to what was implemented for `readerresult`.
|
||||
|
||||
## Key Concepts
|
||||
|
||||
The idiomatic package `github.com/IBM/fp-go/v2/idiomatic/readerioresult` defines:
|
||||
- `ReaderIOResult[R, A]` as `func(R) func() (A, error)` (idiomatic style)
|
||||
- This contrasts with `readerioresult.ReaderIOResult[R, A]` which is `Reader[R, IOResult[A]]` (functional style)
|
||||
|
||||
## Functions to Add
|
||||
|
||||
### In `readerioresult/reader.go`
|
||||
|
||||
Add helper functions at the top:
|
||||
```go
|
||||
func fromReaderIOResultKleisliI[R, A, B any](f RIORI.Kleisli[R, A, B]) Kleisli[R, A, B] {
|
||||
return function.Flow2(f, FromReaderIOResultI[R, B])
|
||||
}
|
||||
|
||||
func fromIOResultKleisliI[A, B any](f IORI.Kleisli[A, B]) ioresult.Kleisli[A, B] {
|
||||
return ioresult.Eitherize1(f)
|
||||
}
|
||||
```
|
||||
|
||||
### Core Conversion Functions
|
||||
|
||||
1. **FromResultI** - Lift `(value, error)` to ReaderIOResult
|
||||
```go
|
||||
func FromResultI[R, A any](a A, err error) ReaderIOResult[R, A]
|
||||
```
|
||||
|
||||
2. **FromIOResultI** - Lift idiomatic IOResult to functional
|
||||
```go
|
||||
func FromIOResultI[R, A any](ioe func() (A, error)) ReaderIOResult[R, A]
|
||||
```
|
||||
|
||||
3. **FromReaderIOResultI** - Convert idiomatic ReaderIOResult to functional
|
||||
```go
|
||||
func FromReaderIOResultI[R, A any](rr RIORI.ReaderIOResult[R, A]) ReaderIOResult[R, A]
|
||||
```
|
||||
|
||||
### Chain Functions
|
||||
|
||||
4. **MonadChainI** / **ChainI** - Chain with idiomatic Kleisli
|
||||
```go
|
||||
func MonadChainI[R, A, B any](ma ReaderIOResult[R, A], f RIORI.Kleisli[R, A, B]) ReaderIOResult[R, B]
|
||||
func ChainI[R, A, B any](f RIORI.Kleisli[R, A, B]) Operator[R, A, B]
|
||||
```
|
||||
|
||||
5. **MonadChainEitherIK** / **ChainEitherIK** - Chain with idiomatic Result functions
|
||||
```go
|
||||
func MonadChainEitherIK[R, A, B any](ma ReaderIOResult[R, A], f func(A) (B, error)) ReaderIOResult[R, B]
|
||||
func ChainEitherIK[R, A, B any](f func(A) (B, error)) Operator[R, A, B]
|
||||
```
|
||||
|
||||
6. **MonadChainIOResultIK** / **ChainIOResultIK** - Chain with idiomatic IOResult
|
||||
```go
|
||||
func MonadChainIOResultIK[R, A, B any](ma ReaderIOResult[R, A], f func(A) func() (B, error)) ReaderIOResult[R, B]
|
||||
func ChainIOResultIK[R, A, B any](f func(A) func() (B, error)) Operator[R, A, B]
|
||||
```
|
||||
|
||||
### Applicative Functions
|
||||
|
||||
7. **MonadApI** / **ApI** - Apply with idiomatic value
|
||||
```go
|
||||
func MonadApI[B, R, A any](fab ReaderIOResult[R, func(A) B], fa RIORI.ReaderIOResult[R, A]) ReaderIOResult[R, B]
|
||||
func ApI[B, R, A any](fa RIORI.ReaderIOResult[R, A]) Operator[R, func(A) B, B]
|
||||
```
|
||||
|
||||
### Error Handling Functions
|
||||
|
||||
8. **OrElseI** - Fallback with idiomatic computation
|
||||
```go
|
||||
func OrElseI[R, A any](onLeft RIORI.Kleisli[R, error, A]) Operator[R, A, A]
|
||||
```
|
||||
|
||||
9. **MonadAltI** / **AltI** - Alternative with idiomatic computation
|
||||
```go
|
||||
func MonadAltI[R, A any](first ReaderIOResult[R, A], second Lazy[RIORI.ReaderIOResult[R, A]]) ReaderIOResult[R, A]
|
||||
func AltI[R, A any](second Lazy[RIORI.ReaderIOResult[R, A]]) Operator[R, A, A]
|
||||
```
|
||||
|
||||
### Flatten Functions
|
||||
|
||||
10. **FlattenI** - Flatten nested idiomatic ReaderIOResult
|
||||
```go
|
||||
func FlattenI[R, A any](mma ReaderIOResult[R, RIORI.ReaderIOResult[R, A]]) ReaderIOResult[R, A]
|
||||
```
|
||||
|
||||
### In `readerioresult/bind.go`
|
||||
|
||||
11. **BindI** - Bind with idiomatic Kleisli
|
||||
```go
|
||||
func BindI[R, S1, S2, T any](setter func(T) func(S1) S2, f RIORI.Kleisli[R, S1, T]) Operator[R, S1, S2]
|
||||
```
|
||||
|
||||
12. **ApIS** - Apply idiomatic value to state
|
||||
```go
|
||||
func ApIS[R, S1, S2, T any](setter func(T) func(S1) S2, fa RIORI.ReaderIOResult[R, T]) Operator[R, S1, S2]
|
||||
```
|
||||
|
||||
13. **ApISL** - Apply idiomatic value using lens
|
||||
```go
|
||||
func ApISL[R, S, T any](lens L.Lens[S, T], fa RIORI.ReaderIOResult[R, T]) Operator[R, S, S]
|
||||
```
|
||||
|
||||
14. **BindIL** - Bind idiomatic with lens
|
||||
```go
|
||||
func BindIL[R, S, T any](lens L.Lens[S, T], f RIORI.Kleisli[R, T, T]) Operator[R, S, S]
|
||||
```
|
||||
|
||||
15. **BindEitherIK** / **BindResultIK** - Bind idiomatic Result
|
||||
```go
|
||||
func BindEitherIK[R, S1, S2, T any](setter func(T) func(S1) S2, f func(S1) (T, error)) Operator[R, S1, S2]
|
||||
func BindResultIK[R, S1, S2, T any](setter func(T) func(S1) S2, f func(S1) (T, error)) Operator[R, S1, S2]
|
||||
```
|
||||
|
||||
16. **BindIOResultIK** - Bind idiomatic IOResult
|
||||
```go
|
||||
func BindIOResultIK[R, S1, S2, T any](setter func(T) func(S1) S2, f func(S1) func() (T, error)) Operator[R, S1, S2]
|
||||
```
|
||||
|
||||
17. **BindToEitherI** / **BindToResultI** - Initialize from idiomatic pair
|
||||
```go
|
||||
func BindToEitherI[R, S1, T any](setter func(T) S1) func(T, error) ReaderIOResult[R, S1]
|
||||
func BindToResultI[R, S1, T any](setter func(T) S1) func(T, error) ReaderIOResult[R, S1]
|
||||
```
|
||||
|
||||
18. **BindToIOResultI** - Initialize from idiomatic IOResult
|
||||
```go
|
||||
func BindToIOResultI[R, S1, T any](setter func(T) S1) func(func() (T, error)) ReaderIOResult[R, S1]
|
||||
```
|
||||
|
||||
19. **ApEitherIS** / **ApResultIS** - Apply idiomatic pair to state
|
||||
```go
|
||||
func ApEitherIS[R, S1, S2, T any](setter func(T) func(S1) S2) func(T, error) Operator[R, S1, S2]
|
||||
func ApResultIS[R, S1, S2, T any](setter func(T) func(S1) S2) func(T, error) Operator[R, S1, S2]
|
||||
```
|
||||
|
||||
20. **ApIOResultIS** - Apply idiomatic IOResult to state
|
||||
```go
|
||||
func ApIOResultIS[R, S1, S2, T any](setter func(T) func(S1) S2, fa func() (T, error)) Operator[R, S1, S2]
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Create `readerioresult/idiomatic_test.go` with:
|
||||
- Tests for each idiomatic function
|
||||
- Success and error cases
|
||||
- Integration tests showing real-world usage patterns
|
||||
- Parallel execution tests where applicable
|
||||
- Complex scenarios combining multiple idiomatic functions
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
1. **High Priority** - Core conversion and chain functions (1-6)
|
||||
2. **Medium Priority** - Bind functions for do-notation (11-16)
|
||||
3. **Low Priority** - Advanced applicative and error handling (7-10, 17-20)
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Seamless Integration** - Mix Go idiomatic code with functional pipelines
|
||||
2. **Gradual Adoption** - Convert code incrementally from idiomatic to functional
|
||||
3. **Interoperability** - Work with existing Go libraries that return `(value, error)`
|
||||
4. **Consistency** - Mirrors the successful pattern from `readerresult`
|
||||
|
||||
## References
|
||||
|
||||
- See `readerresult` package for similar implementations
|
||||
- See `idiomatic/readerresult` for the idiomatic types
|
||||
- See `idiomatic/ioresult` for IO-level idiomatic patterns
|
||||
@@ -205,7 +205,7 @@ The `Compose` function for endomorphisms now follows **mathematical function com
|
||||
```go
|
||||
// Compose executed left-to-right
|
||||
double := N.Mul(2)
|
||||
increment := func(x int) int { return x + 1 }
|
||||
increment := N.Add(1)
|
||||
composed := Compose(double, increment)
|
||||
result := composed(5) // (5 * 2) + 1 = 11
|
||||
```
|
||||
@@ -214,7 +214,7 @@ result := composed(5) // (5 * 2) + 1 = 11
|
||||
```go
|
||||
// Compose executes RIGHT-TO-LEFT (mathematical composition)
|
||||
double := N.Mul(2)
|
||||
increment := func(x int) int { return x + 1 }
|
||||
increment := N.Add(1)
|
||||
composed := Compose(double, increment)
|
||||
result := composed(5) // (5 + 1) * 2 = 12
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestReplicate(t *testing.T) {
|
||||
|
||||
func TestMonadMap(t *testing.T) {
|
||||
src := []int{1, 2, 3}
|
||||
result := MonadMap(src, func(x int) int { return x * 2 })
|
||||
result := MonadMap(src, N.Mul(2))
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
}
|
||||
|
||||
@@ -173,8 +173,8 @@ func TestChain(t *testing.T) {
|
||||
|
||||
func TestMonadAp(t *testing.T) {
|
||||
fns := []func(int) int{
|
||||
func(x int) int { return x * 2 },
|
||||
func(x int) int { return x + 10 },
|
||||
N.Mul(2),
|
||||
N.Add(10),
|
||||
}
|
||||
values := []int{1, 2}
|
||||
result := MonadAp(fns, values)
|
||||
@@ -268,7 +268,7 @@ func TestCopy(t *testing.T) {
|
||||
|
||||
func TestClone(t *testing.T) {
|
||||
src := []int{1, 2, 3}
|
||||
cloner := Clone(func(x int) int { return x * 2 })
|
||||
cloner := Clone(N.Mul(2))
|
||||
result := cloner(src)
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
}
|
||||
|
||||
470
v2/assert/assert.go
Normal file
470
v2/assert/assert.go
Normal file
@@ -0,0 +1,470 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package assert provides functional assertion helpers for testing.
|
||||
//
|
||||
// This package wraps testify/assert functions in a Reader monad pattern,
|
||||
// allowing for composable and functional test assertions. Each assertion
|
||||
// returns a Reader that takes a *testing.T and performs the assertion.
|
||||
//
|
||||
// The package supports:
|
||||
// - Equality and inequality assertions
|
||||
// - Collection assertions (arrays, maps, strings)
|
||||
// - Error handling assertions
|
||||
// - Result type assertions
|
||||
// - Custom predicate assertions
|
||||
// - Composable test suites
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestExample(t *testing.T) {
|
||||
// value := 42
|
||||
// assert.Equal(42)(value)(t) // Curried style
|
||||
//
|
||||
// // Composing multiple assertions
|
||||
// arr := []int{1, 2, 3}
|
||||
// assertions := assert.AllOf([]assert.Reader{
|
||||
// assert.ArrayNotEmpty(arr),
|
||||
// assert.ArrayLength[int](3)(arr),
|
||||
// assert.ArrayContains(2)(arr),
|
||||
// })
|
||||
// assertions(t)
|
||||
// }
|
||||
package assert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/boolean"
|
||||
"github.com/IBM/fp-go/v2/eq"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
// Eq is the equal predicate checking if objects are equal
|
||||
Eq = eq.FromEquals(assert.ObjectsAreEqual)
|
||||
)
|
||||
|
||||
// wrap1 is an internal helper function that wraps testify assertion functions
|
||||
// into the Reader monad pattern with curried parameters.
|
||||
//
|
||||
// It takes a testify assertion function and converts it into a curried function
|
||||
// that first takes an expected value, then an actual value, and finally returns
|
||||
// a Reader that performs the assertion when given a *testing.T.
|
||||
//
|
||||
// Parameters:
|
||||
// - wrapped: The testify assertion function to wrap
|
||||
// - expected: The expected value for comparison
|
||||
// - msgAndArgs: Optional message and arguments for assertion failure
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli function that takes the actual value and returns a Reader
|
||||
func wrap1[T any](wrapped func(t assert.TestingT, expected, actual any, msgAndArgs ...any) bool, expected T, msgAndArgs ...any) Kleisli[T] {
|
||||
return func(actual T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return wrapped(t, expected, actual, msgAndArgs...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NotEqual tests if the expected and the actual values are not equal
|
||||
func NotEqual[T any](expected T) Kleisli[T] {
|
||||
return wrap1(assert.NotEqual, expected)
|
||||
}
|
||||
|
||||
// Equal tests if the expected and the actual values are equal
|
||||
func Equal[T any](expected T) Kleisli[T] {
|
||||
return wrap1(assert.Equal, expected)
|
||||
}
|
||||
|
||||
// ArrayNotEmpty checks if an array is not empty
|
||||
func ArrayNotEmpty[T any](arr []T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.NotEmpty(t, arr)
|
||||
}
|
||||
}
|
||||
|
||||
// RecordNotEmpty checks if an map is not empty
|
||||
func RecordNotEmpty[K comparable, T any](mp map[K]T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.NotEmpty(t, mp)
|
||||
}
|
||||
}
|
||||
|
||||
// StringNotEmpty checks if a string is not empty
|
||||
func StringNotEmpty(s string) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.NotEmpty(t, s)
|
||||
}
|
||||
}
|
||||
|
||||
// ArrayLength tests if an array has the expected length
|
||||
func ArrayLength[T any](expected int) Kleisli[[]T] {
|
||||
return func(actual []T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.Len(t, actual, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RecordLength tests if a map has the expected length
|
||||
func RecordLength[K comparable, T any](expected int) Kleisli[map[K]T] {
|
||||
return func(actual map[K]T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.Len(t, actual, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// StringLength tests if a string has the expected length
|
||||
func StringLength[K comparable, T any](expected int) Kleisli[string] {
|
||||
return func(actual string) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.Len(t, actual, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NoError validates that there is no error
|
||||
func NoError(err error) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Error validates that there is an error
|
||||
func Error(err error) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Success checks if a [Result] represents success
|
||||
func Success[T any](res Result[T]) Reader {
|
||||
return NoError(result.ToError(res))
|
||||
}
|
||||
|
||||
// Failure checks if a [Result] represents failure
|
||||
func Failure[T any](res Result[T]) Reader {
|
||||
return Error(result.ToError(res))
|
||||
}
|
||||
|
||||
// ArrayContains tests if a value is contained in an array
|
||||
func ArrayContains[T any](expected T) Kleisli[[]T] {
|
||||
return func(actual []T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.Contains(t, actual, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ContainsKey tests if a key is contained in a map
|
||||
func ContainsKey[T any, K comparable](expected K) Kleisli[map[K]T] {
|
||||
return func(actual map[K]T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.Contains(t, actual, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NotContainsKey tests if a key is not contained in a map
|
||||
func NotContainsKey[T any, K comparable](expected K) Kleisli[map[K]T] {
|
||||
return func(actual map[K]T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.NotContains(t, actual, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// That asserts that a particular predicate matches
|
||||
func That[T any](pred Predicate[T]) Kleisli[T] {
|
||||
return func(a T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
if pred(a) {
|
||||
return true
|
||||
}
|
||||
return assert.Fail(t, fmt.Sprintf("Preficate %v does not match value %v", pred, a))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AllOf combines multiple assertion Readers into a single Reader that passes
|
||||
// only if all assertions pass.
|
||||
//
|
||||
// This function uses boolean AND logic (MonoidAll) to combine the results of
|
||||
// all assertions. If any assertion fails, the combined assertion fails.
|
||||
//
|
||||
// This is useful for grouping related assertions together and ensuring all
|
||||
// conditions are met.
|
||||
//
|
||||
// Parameters:
|
||||
// - readers: Array of assertion Readers to combine
|
||||
//
|
||||
// Returns:
|
||||
// - A single Reader that performs all assertions and returns true only if all pass
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestUser(t *testing.T) {
|
||||
// user := User{Name: "Alice", Age: 30, Active: true}
|
||||
// assertions := assert.AllOf([]assert.Reader{
|
||||
// assert.Equal("Alice")(user.Name),
|
||||
// assert.Equal(30)(user.Age),
|
||||
// assert.Equal(true)(user.Active),
|
||||
// })
|
||||
// assertions(t)
|
||||
// }
|
||||
//
|
||||
//go:inline
|
||||
func AllOf(readers []Reader) Reader {
|
||||
return reader.MonadReduceArrayM(readers, boolean.MonoidAll)
|
||||
}
|
||||
|
||||
// RunAll executes a map of named test cases, running each as a subtest.
|
||||
//
|
||||
// This function creates a Reader that runs multiple named test cases using
|
||||
// Go's t.Run for proper test isolation and reporting. Each test case is
|
||||
// executed as a separate subtest with its own name.
|
||||
//
|
||||
// The function returns true only if all subtests pass. This allows for
|
||||
// better test organization and clearer test output.
|
||||
//
|
||||
// Parameters:
|
||||
// - testcases: Map of test names to assertion Readers
|
||||
//
|
||||
// Returns:
|
||||
// - A Reader that executes all named test cases and returns true if all pass
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestMathOperations(t *testing.T) {
|
||||
// testcases := map[string]assert.Reader{
|
||||
// "addition": assert.Equal(4)(2 + 2),
|
||||
// "multiplication": assert.Equal(6)(2 * 3),
|
||||
// "subtraction": assert.Equal(1)(3 - 2),
|
||||
// }
|
||||
// assert.RunAll(testcases)(t)
|
||||
// }
|
||||
//
|
||||
//go:inline
|
||||
func RunAll(testcases map[string]Reader) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
current := true
|
||||
for k, r := range testcases {
|
||||
current = current && t.Run(k, func(t1 *testing.T) {
|
||||
r(t1)
|
||||
})
|
||||
}
|
||||
return current
|
||||
}
|
||||
}
|
||||
|
||||
// Local transforms a Reader that works on type R1 into a Reader that works on type R2,
|
||||
// by providing a function that converts R2 to R1. This allows you to focus a test on a
|
||||
// specific property or subset of a larger data structure.
|
||||
//
|
||||
// This is particularly useful when you have an assertion that operates on a specific field
|
||||
// or property, and you want to apply it to a complete object. Instead of extracting the
|
||||
// property and then asserting on it, you can transform the assertion to work directly
|
||||
// on the whole object.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that extracts or transforms R2 into R1
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms a Reader[R1, Reader] into a Reader[R2, Reader]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// // Create an assertion that checks if age is positive
|
||||
// ageIsPositive := assert.That(func(age int) bool { return age > 0 })
|
||||
//
|
||||
// // Focus this assertion on the Age field of User
|
||||
// userAgeIsPositive := assert.Local(func(u User) int { return u.Age })(ageIsPositive)
|
||||
//
|
||||
// // Now we can test the whole User object
|
||||
// user := User{Name: "Alice", Age: 30}
|
||||
// userAgeIsPositive(user)(t)
|
||||
//
|
||||
//go:inline
|
||||
func Local[R1, R2 any](f func(R2) R1) func(Kleisli[R1]) Kleisli[R2] {
|
||||
return reader.Local[Reader](f)
|
||||
}
|
||||
|
||||
// LocalL is similar to Local but uses a Lens to focus on a specific property.
|
||||
// A Lens is a functional programming construct that provides a composable way to
|
||||
// focus on a part of a data structure.
|
||||
//
|
||||
// This function is particularly useful when you want to focus a test on a specific
|
||||
// field of a struct using a lens, making the code more declarative and composable.
|
||||
// Lenses are often code-generated or predefined for common data structures.
|
||||
//
|
||||
// Parameters:
|
||||
// - l: A Lens that focuses from type S to type T
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms a Reader[T, Reader] into a Reader[S, Reader]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Email string
|
||||
// }
|
||||
//
|
||||
// // Assume we have a lens that focuses on the Email field
|
||||
// var emailLens = lens.Prop[Person, string]("Email")
|
||||
//
|
||||
// // Create an assertion for email format
|
||||
// validEmail := assert.That(func(email string) bool {
|
||||
// return strings.Contains(email, "@")
|
||||
// })
|
||||
//
|
||||
// // Focus this assertion on the Email property using a lens
|
||||
// validPersonEmail := assert.LocalL(emailLens)(validEmail)
|
||||
//
|
||||
// // Test a Person object
|
||||
// person := Person{Name: "Bob", Email: "bob@example.com"}
|
||||
// validPersonEmail(person)(t)
|
||||
//
|
||||
//go:inline
|
||||
func LocalL[S, T any](l Lens[S, T]) func(Kleisli[T]) Kleisli[S] {
|
||||
return reader.Local[Reader](l.Get)
|
||||
}
|
||||
|
||||
// fromOptionalGetter is an internal helper that creates an assertion Reader from
|
||||
// an optional getter function. It asserts that the optional value is present (Some).
|
||||
func fromOptionalGetter[S, T any](getter func(S) option.Option[T], msgAndArgs ...any) Kleisli[S] {
|
||||
return func(s S) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.True(t, option.IsSome(getter(s)), msgAndArgs...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FromOptional creates an assertion that checks if an Optional can successfully extract a value.
|
||||
// An Optional is an optic that represents an optional reference to a subpart of a data structure.
|
||||
//
|
||||
// This function is useful when you have an Optional optic and want to assert that the optional
|
||||
// value is present (Some) rather than absent (None). The assertion passes if the Optional's
|
||||
// GetOption returns Some, and fails if it returns None.
|
||||
//
|
||||
// This enables property-focused testing where you verify that a particular optional field or
|
||||
// sub-structure exists and is accessible.
|
||||
//
|
||||
// Parameters:
|
||||
// - opt: An Optional optic that focuses from type S to type T
|
||||
//
|
||||
// Returns:
|
||||
// - A Reader that asserts the optional value is present when applied to a value of type S
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Database *DatabaseConfig // Optional field
|
||||
// }
|
||||
//
|
||||
// type DatabaseConfig struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
//
|
||||
// // Create an Optional that focuses on the Database field
|
||||
// dbOptional := optional.MakeOptional(
|
||||
// func(c Config) option.Option[*DatabaseConfig] {
|
||||
// if c.Database != nil {
|
||||
// return option.Some(c.Database)
|
||||
// }
|
||||
// return option.None[*DatabaseConfig]()
|
||||
// },
|
||||
// func(c Config, db *DatabaseConfig) Config {
|
||||
// c.Database = db
|
||||
// return c
|
||||
// },
|
||||
// )
|
||||
//
|
||||
// // Assert that the database config is present
|
||||
// hasDatabaseConfig := assert.FromOptional(dbOptional)
|
||||
//
|
||||
// config := Config{Database: &DatabaseConfig{Host: "localhost", Port: 5432}}
|
||||
// hasDatabaseConfig(config)(t) // Passes
|
||||
//
|
||||
// emptyConfig := Config{Database: nil}
|
||||
// hasDatabaseConfig(emptyConfig)(t) // Fails
|
||||
//
|
||||
//go:inline
|
||||
func FromOptional[S, T any](opt Optional[S, T]) reader.Reader[S, Reader] {
|
||||
return fromOptionalGetter(opt.GetOption, "Optional: %s", opt)
|
||||
}
|
||||
|
||||
// FromPrism creates an assertion that checks if a Prism can successfully extract a value.
|
||||
// A Prism is an optic used to select part of a sum type (tagged union or variant).
|
||||
//
|
||||
// This function is useful when you have a Prism optic and want to assert that a value
|
||||
// matches a specific variant of a sum type. The assertion passes if the Prism's GetOption
|
||||
// returns Some (meaning the value is of the expected variant), and fails if it returns None
|
||||
// (meaning the value is a different variant).
|
||||
//
|
||||
// This enables variant-focused testing where you verify that a value is of a particular
|
||||
// type or matches a specific condition within a sum type.
|
||||
//
|
||||
// Parameters:
|
||||
// - p: A Prism optic that focuses from type S to type T
|
||||
//
|
||||
// Returns:
|
||||
// - A Reader that asserts the prism successfully extracts when applied to a value of type S
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Result interface{ isResult() }
|
||||
// type Success struct{ Value int }
|
||||
// type Failure struct{ Error string }
|
||||
//
|
||||
// func (Success) isResult() {}
|
||||
// func (Failure) isResult() {}
|
||||
//
|
||||
// // Create a Prism that focuses on Success variant
|
||||
// successPrism := prism.MakePrism(
|
||||
// func(r Result) option.Option[int] {
|
||||
// if s, ok := r.(Success); ok {
|
||||
// return option.Some(s.Value)
|
||||
// }
|
||||
// return option.None[int]()
|
||||
// },
|
||||
// func(v int) Result { return Success{Value: v} },
|
||||
// )
|
||||
//
|
||||
// // Assert that the result is a Success
|
||||
// isSuccess := assert.FromPrism(successPrism)
|
||||
//
|
||||
// result1 := Success{Value: 42}
|
||||
// isSuccess(result1)(t) // Passes
|
||||
//
|
||||
// result2 := Failure{Error: "something went wrong"}
|
||||
// isSuccess(result2)(t) // Fails
|
||||
//
|
||||
//go:inline
|
||||
func FromPrism[S, T any](p Prism[S, T]) reader.Reader[S, Reader] {
|
||||
return fromOptionalGetter(p.GetOption, "Prism: %s", p)
|
||||
}
|
||||
@@ -16,94 +16,676 @@
|
||||
package assert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/eq"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
errTest = fmt.Errorf("test failure")
|
||||
|
||||
// Eq is the equal predicate checking if objects are equal
|
||||
Eq = eq.FromEquals(assert.ObjectsAreEqual)
|
||||
)
|
||||
|
||||
func wrap1[T any](wrapped func(t assert.TestingT, expected, actual any, msgAndArgs ...any) bool, t *testing.T, expected T) result.Kleisli[T, T] {
|
||||
return func(actual T) Result[T] {
|
||||
ok := wrapped(t, expected, actual)
|
||||
if ok {
|
||||
return result.Of(actual)
|
||||
func TestEqual(t *testing.T) {
|
||||
t.Run("should pass when values are equal", func(t *testing.T) {
|
||||
result := Equal(42)(42)(t)
|
||||
if !result {
|
||||
t.Error("Expected Equal to pass for equal values")
|
||||
}
|
||||
return result.Left[T](errTest)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// NotEqual tests if the expected and the actual values are not equal
|
||||
func NotEqual[T any](t *testing.T, expected T) result.Kleisli[T, T] {
|
||||
return wrap1(assert.NotEqual, t, expected)
|
||||
}
|
||||
|
||||
// Equal tests if the expected and the actual values are equal
|
||||
func Equal[T any](t *testing.T, expected T) result.Kleisli[T, T] {
|
||||
return wrap1(assert.Equal, t, expected)
|
||||
}
|
||||
|
||||
// Length tests if an array has the expected length
|
||||
func Length[T any](t *testing.T, expected int) result.Kleisli[[]T, []T] {
|
||||
return func(actual []T) Result[[]T] {
|
||||
ok := assert.Len(t, actual, expected)
|
||||
if ok {
|
||||
return result.Of(actual)
|
||||
t.Run("should fail when values are not equal", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
result := Equal(42)(43)(mockT)
|
||||
if result {
|
||||
t.Error("Expected Equal to fail for different values")
|
||||
}
|
||||
return result.Left[[]T](errTest)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with strings", func(t *testing.T) {
|
||||
result := Equal("hello")("hello")(t)
|
||||
if !result {
|
||||
t.Error("Expected Equal to pass for equal strings")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// NoError validates that there is no error
|
||||
func NoError[T any](t *testing.T) result.Operator[T, T] {
|
||||
return func(actual Result[T]) Result[T] {
|
||||
return result.MonadFold(actual, func(e error) Result[T] {
|
||||
assert.NoError(t, e)
|
||||
return result.Left[T](e)
|
||||
}, func(value T) Result[T] {
|
||||
assert.NoError(t, nil)
|
||||
return result.Of(value)
|
||||
func TestNotEqual(t *testing.T) {
|
||||
t.Run("should pass when values are not equal", func(t *testing.T) {
|
||||
result := NotEqual(42)(43)(t)
|
||||
if !result {
|
||||
t.Error("Expected NotEqual to pass for different values")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when values are equal", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
result := NotEqual(42)(42)(mockT)
|
||||
if result {
|
||||
t.Error("Expected NotEqual to fail for equal values")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayNotEmpty(t *testing.T) {
|
||||
t.Run("should pass for non-empty array", func(t *testing.T) {
|
||||
arr := []int{1, 2, 3}
|
||||
result := ArrayNotEmpty(arr)(t)
|
||||
if !result {
|
||||
t.Error("Expected ArrayNotEmpty to pass for non-empty array")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail for empty array", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
arr := []int{}
|
||||
result := ArrayNotEmpty(arr)(mockT)
|
||||
if result {
|
||||
t.Error("Expected ArrayNotEmpty to fail for empty array")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRecordNotEmpty(t *testing.T) {
|
||||
t.Run("should pass for non-empty map", func(t *testing.T) {
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
result := RecordNotEmpty(mp)(t)
|
||||
if !result {
|
||||
t.Error("Expected RecordNotEmpty to pass for non-empty map")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail for empty map", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
mp := map[string]int{}
|
||||
result := RecordNotEmpty(mp)(mockT)
|
||||
if result {
|
||||
t.Error("Expected RecordNotEmpty to fail for empty map")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayLength(t *testing.T) {
|
||||
t.Run("should pass when length matches", func(t *testing.T) {
|
||||
arr := []int{1, 2, 3}
|
||||
result := ArrayLength[int](3)(arr)(t)
|
||||
if !result {
|
||||
t.Error("Expected ArrayLength to pass when length matches")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when length doesn't match", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
arr := []int{1, 2, 3}
|
||||
result := ArrayLength[int](5)(arr)(mockT)
|
||||
if result {
|
||||
t.Error("Expected ArrayLength to fail when length doesn't match")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with empty array", func(t *testing.T) {
|
||||
arr := []string{}
|
||||
result := ArrayLength[string](0)(arr)(t)
|
||||
if !result {
|
||||
t.Error("Expected ArrayLength to pass for empty array with expected length 0")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRecordLength(t *testing.T) {
|
||||
t.Run("should pass when map length matches", func(t *testing.T) {
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
result := RecordLength[string, int](2)(mp)(t)
|
||||
if !result {
|
||||
t.Error("Expected RecordLength to pass when length matches")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when map length doesn't match", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
mp := map[string]int{"a": 1}
|
||||
result := RecordLength[string, int](3)(mp)(mockT)
|
||||
if result {
|
||||
t.Error("Expected RecordLength to fail when length doesn't match")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestStringLength(t *testing.T) {
|
||||
t.Run("should pass when string length matches", func(t *testing.T) {
|
||||
str := "hello"
|
||||
result := StringLength[string, int](5)(str)(t)
|
||||
if !result {
|
||||
t.Error("Expected StringLength to pass when length matches")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when string length doesn't match", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
str := "hello"
|
||||
result := StringLength[string, int](10)(str)(mockT)
|
||||
if result {
|
||||
t.Error("Expected StringLength to fail when length doesn't match")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with empty string", func(t *testing.T) {
|
||||
str := ""
|
||||
result := StringLength[string, int](0)(str)(t)
|
||||
if !result {
|
||||
t.Error("Expected StringLength to pass for empty string with expected length 0")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNoError(t *testing.T) {
|
||||
t.Run("should pass when error is nil", func(t *testing.T) {
|
||||
result := NoError(nil)(t)
|
||||
if !result {
|
||||
t.Error("Expected NoError to pass when error is nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when error is not nil", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
err := errors.New("test error")
|
||||
result := NoError(err)(mockT)
|
||||
if result {
|
||||
t.Error("Expected NoError to fail when error is not nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
t.Run("should pass when error is not nil", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
result := Error(err)(t)
|
||||
if !result {
|
||||
t.Error("Expected Error to pass when error is not nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when error is nil", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
result := Error(nil)(mockT)
|
||||
if result {
|
||||
t.Error("Expected Error to fail when error is nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSuccess(t *testing.T) {
|
||||
t.Run("should pass for successful result", func(t *testing.T) {
|
||||
res := result.Of(42)
|
||||
result := Success(res)(t)
|
||||
if !result {
|
||||
t.Error("Expected Success to pass for successful result")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail for error result", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
res := result.Left[int](errors.New("test error"))
|
||||
result := Success(res)(mockT)
|
||||
if result {
|
||||
t.Error("Expected Success to fail for error result")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFailure(t *testing.T) {
|
||||
t.Run("should pass for error result", func(t *testing.T) {
|
||||
res := result.Left[int](errors.New("test error"))
|
||||
result := Failure(res)(t)
|
||||
if !result {
|
||||
t.Error("Expected Failure to pass for error result")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail for successful result", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
res := result.Of(42)
|
||||
result := Failure(res)(mockT)
|
||||
if result {
|
||||
t.Error("Expected Failure to fail for successful result")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayContains(t *testing.T) {
|
||||
t.Run("should pass when element is in array", func(t *testing.T) {
|
||||
arr := []int{1, 2, 3, 4, 5}
|
||||
result := ArrayContains(3)(arr)(t)
|
||||
if !result {
|
||||
t.Error("Expected ArrayContains to pass when element is in array")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when element is not in array", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
arr := []int{1, 2, 3}
|
||||
result := ArrayContains(10)(arr)(mockT)
|
||||
if result {
|
||||
t.Error("Expected ArrayContains to fail when element is not in array")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with strings", func(t *testing.T) {
|
||||
arr := []string{"apple", "banana", "cherry"}
|
||||
result := ArrayContains("banana")(arr)(t)
|
||||
if !result {
|
||||
t.Error("Expected ArrayContains to pass for string element")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestContainsKey(t *testing.T) {
|
||||
t.Run("should pass when key exists in map", func(t *testing.T) {
|
||||
mp := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
result := ContainsKey[int]("b")(mp)(t)
|
||||
if !result {
|
||||
t.Error("Expected ContainsKey to pass when key exists")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when key doesn't exist in map", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
result := ContainsKey[int]("z")(mp)(mockT)
|
||||
if result {
|
||||
t.Error("Expected ContainsKey to fail when key doesn't exist")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNotContainsKey(t *testing.T) {
|
||||
t.Run("should pass when key doesn't exist in map", func(t *testing.T) {
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
result := NotContainsKey[int]("z")(mp)(t)
|
||||
if !result {
|
||||
t.Error("Expected NotContainsKey to pass when key doesn't exist")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when key exists in map", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
result := NotContainsKey[int]("a")(mp)(mockT)
|
||||
if result {
|
||||
t.Error("Expected NotContainsKey to fail when key exists")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestThat(t *testing.T) {
|
||||
t.Run("should pass when predicate is true", func(t *testing.T) {
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
result := That(isEven)(42)(t)
|
||||
if !result {
|
||||
t.Error("Expected That to pass when predicate is true")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when predicate is false", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
result := That(isEven)(43)(mockT)
|
||||
if result {
|
||||
t.Error("Expected That to fail when predicate is false")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with string predicates", func(t *testing.T) {
|
||||
startsWithH := func(s string) bool { return len(s) > 0 && s[0] == 'h' }
|
||||
result := That(startsWithH)("hello")(t)
|
||||
if !result {
|
||||
t.Error("Expected That to pass for string predicate")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAllOf(t *testing.T) {
|
||||
t.Run("should pass when all assertions pass", func(t *testing.T) {
|
||||
assertions := AllOf([]Reader{
|
||||
Equal(42)(42),
|
||||
Equal("hello")("hello"),
|
||||
ArrayNotEmpty([]int{1, 2, 3}),
|
||||
})
|
||||
}
|
||||
result := assertions(t)
|
||||
if !result {
|
||||
t.Error("Expected AllOf to pass when all assertions pass")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when any assertion fails", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
assertions := AllOf([]Reader{
|
||||
Equal(42)(42),
|
||||
Equal("hello")("goodbye"),
|
||||
ArrayNotEmpty([]int{1, 2, 3}),
|
||||
})
|
||||
result := assertions(mockT)
|
||||
if result {
|
||||
t.Error("Expected AllOf to fail when any assertion fails")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with empty array", func(t *testing.T) {
|
||||
assertions := AllOf([]Reader{})
|
||||
result := assertions(t)
|
||||
if !result {
|
||||
t.Error("Expected AllOf to pass for empty array")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should combine multiple array assertions", func(t *testing.T) {
|
||||
arr := []int{1, 2, 3, 4, 5}
|
||||
assertions := AllOf([]Reader{
|
||||
ArrayNotEmpty(arr),
|
||||
ArrayLength[int](5)(arr),
|
||||
ArrayContains(3)(arr),
|
||||
})
|
||||
result := assertions(t)
|
||||
if !result {
|
||||
t.Error("Expected AllOf to pass for multiple array assertions")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ArrayContains tests if a value is contained in an array
|
||||
func ArrayContains[T any](t *testing.T, expected T) result.Kleisli[[]T, []T] {
|
||||
return func(actual []T) Result[[]T] {
|
||||
ok := assert.Contains(t, actual, expected)
|
||||
if ok {
|
||||
return result.Of(actual)
|
||||
func TestRunAll(t *testing.T) {
|
||||
t.Run("should run all named test cases", func(t *testing.T) {
|
||||
testcases := map[string]Reader{
|
||||
"equality": Equal(42)(42),
|
||||
"string_check": Equal("test")("test"),
|
||||
"array_check": ArrayNotEmpty([]int{1, 2, 3}),
|
||||
}
|
||||
return result.Left[[]T](errTest)
|
||||
}
|
||||
result := RunAll(testcases)(t)
|
||||
if !result {
|
||||
t.Error("Expected RunAll to pass when all test cases pass")
|
||||
}
|
||||
})
|
||||
|
||||
// Note: Testing failure behavior of RunAll is tricky because subtests
|
||||
// will actually fail in the test framework. The function works correctly
|
||||
// as demonstrated by the passing test above.
|
||||
|
||||
t.Run("should work with empty test cases", func(t *testing.T) {
|
||||
testcases := map[string]Reader{}
|
||||
result := RunAll(testcases)(t)
|
||||
if !result {
|
||||
t.Error("Expected RunAll to pass for empty test cases")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ContainsKey tests if a key is contained in a map
|
||||
func ContainsKey[T any, K comparable](t *testing.T, expected K) result.Kleisli[map[K]T, map[K]T] {
|
||||
return func(actual map[K]T) Result[map[K]T] {
|
||||
ok := assert.Contains(t, actual, expected)
|
||||
if ok {
|
||||
return result.Of(actual)
|
||||
func TestEq(t *testing.T) {
|
||||
t.Run("should return true for equal values", func(t *testing.T) {
|
||||
if !Eq.Equals(42, 42) {
|
||||
t.Error("Expected Eq to return true for equal integers")
|
||||
}
|
||||
return result.Left[map[K]T](errTest)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should return false for different values", func(t *testing.T) {
|
||||
if Eq.Equals(42, 43) {
|
||||
t.Error("Expected Eq to return false for different integers")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with strings", func(t *testing.T) {
|
||||
if !Eq.Equals("hello", "hello") {
|
||||
t.Error("Expected Eq to return true for equal strings")
|
||||
}
|
||||
if Eq.Equals("hello", "world") {
|
||||
t.Error("Expected Eq to return false for different strings")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with slices", func(t *testing.T) {
|
||||
arr1 := []int{1, 2, 3}
|
||||
arr2 := []int{1, 2, 3}
|
||||
if !Eq.Equals(arr1, arr2) {
|
||||
t.Error("Expected Eq to return true for equal slices")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// NotContainsKey tests if a key is not contained in a map
|
||||
func NotContainsKey[T any, K comparable](t *testing.T, expected K) result.Kleisli[map[K]T, map[K]T] {
|
||||
return func(actual map[K]T) Result[map[K]T] {
|
||||
ok := assert.NotContains(t, actual, expected)
|
||||
if ok {
|
||||
return result.Of(actual)
|
||||
}
|
||||
return result.Left[map[K]T](errTest)
|
||||
func TestLocal(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("should focus assertion on a property", func(t *testing.T) {
|
||||
// Create an assertion that checks if age is positive
|
||||
ageIsPositive := That(func(age int) bool { return age > 0 })
|
||||
|
||||
// Focus this assertion on the Age field of User
|
||||
userAgeIsPositive := Local(func(u User) int { return u.Age })(ageIsPositive)
|
||||
|
||||
// Test with a user who has a positive age
|
||||
user := User{Name: "Alice", Age: 30}
|
||||
result := userAgeIsPositive(user)(t)
|
||||
if !result {
|
||||
t.Error("Expected focused assertion to pass for positive age")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when focused property doesn't match", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
ageIsPositive := That(func(age int) bool { return age > 0 })
|
||||
userAgeIsPositive := Local(func(u User) int { return u.Age })(ageIsPositive)
|
||||
|
||||
// Test with a user who has zero age
|
||||
user := User{Name: "Bob", Age: 0}
|
||||
result := userAgeIsPositive(user)(mockT)
|
||||
if result {
|
||||
t.Error("Expected focused assertion to fail for zero age")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should compose with other assertions", func(t *testing.T) {
|
||||
// Create multiple focused assertions
|
||||
nameNotEmpty := Local(func(u User) string { return u.Name })(
|
||||
That(func(name string) bool { return len(name) > 0 }),
|
||||
)
|
||||
ageInRange := Local(func(u User) int { return u.Age })(
|
||||
That(func(age int) bool { return age >= 18 && age <= 100 }),
|
||||
)
|
||||
|
||||
user := User{Name: "Charlie", Age: 25}
|
||||
assertions := AllOf([]Reader{
|
||||
nameNotEmpty(user),
|
||||
ageInRange(user),
|
||||
})
|
||||
|
||||
result := assertions(t)
|
||||
if !result {
|
||||
t.Error("Expected composed focused assertions to pass")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with Equal assertion", func(t *testing.T) {
|
||||
// Focus Equal assertion on Name field
|
||||
nameIsAlice := Local(func(u User) string { return u.Name })(Equal("Alice"))
|
||||
|
||||
user := User{Name: "Alice", Age: 30}
|
||||
result := nameIsAlice(user)(t)
|
||||
if !result {
|
||||
t.Error("Expected focused Equal assertion to pass")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLocalL(t *testing.T) {
|
||||
// Note: LocalL requires lens package which provides lens operations.
|
||||
// This test demonstrates the concept, but actual usage would require
|
||||
// proper lens definitions.
|
||||
|
||||
t.Run("conceptual test for LocalL", func(t *testing.T) {
|
||||
// LocalL is similar to Local but uses lenses for focusing.
|
||||
// It would be used like:
|
||||
// validEmail := That(func(email string) bool { return strings.Contains(email, "@") })
|
||||
// validPersonEmail := LocalL(emailLens)(validEmail)
|
||||
//
|
||||
// The actual implementation would require lens definitions from the lens package.
|
||||
// This test serves as documentation for the intended usage.
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromOptional(t *testing.T) {
|
||||
type DatabaseConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Database *DatabaseConfig
|
||||
}
|
||||
|
||||
// Create an Optional that focuses on the Database field
|
||||
dbOptional := Optional[Config, *DatabaseConfig]{
|
||||
GetOption: func(c Config) option.Option[*DatabaseConfig] {
|
||||
if c.Database != nil {
|
||||
return option.Of(c.Database)
|
||||
}
|
||||
return option.None[*DatabaseConfig]()
|
||||
},
|
||||
Set: func(db *DatabaseConfig) func(Config) Config {
|
||||
return func(c Config) Config {
|
||||
c.Database = db
|
||||
return c
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("should pass when optional value is present", func(t *testing.T) {
|
||||
config := Config{Database: &DatabaseConfig{Host: "localhost", Port: 5432}}
|
||||
hasDatabaseConfig := FromOptional(dbOptional)
|
||||
result := hasDatabaseConfig(config)(t)
|
||||
if !result {
|
||||
t.Error("Expected FromOptional to pass when optional value is present")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when optional value is absent", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
emptyConfig := Config{Database: nil}
|
||||
hasDatabaseConfig := FromOptional(dbOptional)
|
||||
result := hasDatabaseConfig(emptyConfig)(mockT)
|
||||
if result {
|
||||
t.Error("Expected FromOptional to fail when optional value is absent")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with nested optionals", func(t *testing.T) {
|
||||
type AdvancedSettings struct {
|
||||
Cache bool
|
||||
}
|
||||
|
||||
type Settings struct {
|
||||
Advanced *AdvancedSettings
|
||||
}
|
||||
|
||||
advancedOptional := Optional[Settings, *AdvancedSettings]{
|
||||
GetOption: func(s Settings) option.Option[*AdvancedSettings] {
|
||||
if s.Advanced != nil {
|
||||
return option.Of(s.Advanced)
|
||||
}
|
||||
return option.None[*AdvancedSettings]()
|
||||
},
|
||||
Set: func(adv *AdvancedSettings) func(Settings) Settings {
|
||||
return func(s Settings) Settings {
|
||||
s.Advanced = adv
|
||||
return s
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
settings := Settings{Advanced: &AdvancedSettings{Cache: true}}
|
||||
hasAdvanced := FromOptional(advancedOptional)
|
||||
result := hasAdvanced(settings)(t)
|
||||
if !result {
|
||||
t.Error("Expected FromOptional to pass for nested optional")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Helper types for Prism testing
|
||||
type PrismTestResult interface {
|
||||
isPrismTestResult()
|
||||
}
|
||||
|
||||
type PrismTestSuccess struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
type PrismTestFailure struct {
|
||||
Error string
|
||||
}
|
||||
|
||||
func (PrismTestSuccess) isPrismTestResult() {}
|
||||
func (PrismTestFailure) isPrismTestResult() {}
|
||||
|
||||
func TestFromPrism(t *testing.T) {
|
||||
// Create a Prism that focuses on Success variant using prism.MakePrism
|
||||
successPrism := prism.MakePrism(
|
||||
func(r PrismTestResult) option.Option[int] {
|
||||
if s, ok := r.(PrismTestSuccess); ok {
|
||||
return option.Of(s.Value)
|
||||
}
|
||||
return option.None[int]()
|
||||
},
|
||||
func(v int) PrismTestResult {
|
||||
return PrismTestSuccess{Value: v}
|
||||
},
|
||||
)
|
||||
|
||||
// Create a Prism that focuses on Failure variant
|
||||
failurePrism := prism.MakePrism(
|
||||
func(r PrismTestResult) option.Option[string] {
|
||||
if f, ok := r.(PrismTestFailure); ok {
|
||||
return option.Of(f.Error)
|
||||
}
|
||||
return option.None[string]()
|
||||
},
|
||||
func(err string) PrismTestResult {
|
||||
return PrismTestFailure{Error: err}
|
||||
},
|
||||
)
|
||||
|
||||
t.Run("should pass when prism successfully extracts", func(t *testing.T) {
|
||||
result := PrismTestSuccess{Value: 42}
|
||||
isSuccess := FromPrism(successPrism)
|
||||
testResult := isSuccess(result)(t)
|
||||
if !testResult {
|
||||
t.Error("Expected FromPrism to pass when prism extracts successfully")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when prism cannot extract", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
result := PrismTestFailure{Error: "something went wrong"}
|
||||
isSuccess := FromPrism(successPrism)
|
||||
testResult := isSuccess(result)(mockT)
|
||||
if testResult {
|
||||
t.Error("Expected FromPrism to fail when prism cannot extract")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with failure prism", func(t *testing.T) {
|
||||
result := PrismTestFailure{Error: "test error"}
|
||||
isFailure := FromPrism(failurePrism)
|
||||
testResult := isFailure(result)(t)
|
||||
if !testResult {
|
||||
t.Error("Expected FromPrism to pass for failure prism on failure result")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail with failure prism on success result", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
result := PrismTestSuccess{Value: 100}
|
||||
isFailure := FromPrism(failurePrism)
|
||||
testResult := isFailure(result)(mockT)
|
||||
if testResult {
|
||||
t.Error("Expected FromPrism to fail for failure prism on success result")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
package assert
|
||||
|
||||
import "github.com/IBM/fp-go/v2/result"
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/optics/optional"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
type (
|
||||
Result[T any] = result.Result[T]
|
||||
Result[T any] = result.Result[T]
|
||||
Reader = reader.Reader[*testing.T, bool]
|
||||
Kleisli[T any] = reader.Reader[T, Reader]
|
||||
Predicate[T any] = predicate.Predicate[T]
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
Optional[S, T any] = optional.Optional[S, T]
|
||||
Prism[S, T any] = prism.Prism[S, T]
|
||||
)
|
||||
|
||||
@@ -27,9 +27,9 @@ import (
|
||||
|
||||
// resourceState tracks the lifecycle of resources for testing
|
||||
type resourceState struct {
|
||||
resourcesCreated int
|
||||
resourcesCreated int
|
||||
resourcesReleased int
|
||||
lastError error
|
||||
lastError error
|
||||
}
|
||||
|
||||
// mockResource represents a test resource
|
||||
|
||||
@@ -68,7 +68,7 @@ func Of[S, A any](a A) StateReaderIOResult[S, A] {
|
||||
//
|
||||
// result := statereaderioresult.MonadMap(
|
||||
// statereaderioresult.Of[AppState](21),
|
||||
// func(x int) int { return x * 2 },
|
||||
// N.Mul(2),
|
||||
// ) // Result contains 42
|
||||
func MonadMap[S, A, B any](fa StateReaderIOResult[S, A], f func(A) B) StateReaderIOResult[S, B] {
|
||||
return statet.MonadMap[StateReaderIOResult[S, A], StateReaderIOResult[S, B]](
|
||||
@@ -83,7 +83,7 @@ func MonadMap[S, A, B any](fa StateReaderIOResult[S, A], f func(A) B) StateReade
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// double := statereaderioresult.Map[AppState](func(x int) int { return x * 2 })
|
||||
// double := statereaderioresult.Map[AppState](N.Mul(2))
|
||||
// result := function.Pipe1(statereaderioresult.Of[AppState](21), double)
|
||||
func Map[S, A, B any](f func(A) B) Operator[S, A, B] {
|
||||
return statet.Map[StateReaderIOResult[S, A], StateReaderIOResult[S, B]](
|
||||
@@ -135,7 +135,7 @@ func Chain[S, A, B any](f Kleisli[S, A, B]) Operator[S, A, B] {
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// fab := statereaderioresult.Of[AppState](func(x int) int { return x * 2 })
|
||||
// fab := statereaderioresult.Of[AppState](N.Mul(2))
|
||||
// fa := statereaderioresult.Of[AppState](21)
|
||||
// result := statereaderioresult.MonadAp(fab, fa) // Result contains 42
|
||||
func MonadAp[B, S, A any](fab StateReaderIOResult[S, func(A) B], fa StateReaderIOResult[S, A]) StateReaderIOResult[S, B] {
|
||||
|
||||
@@ -137,7 +137,7 @@ func TestChain(t *testing.T) {
|
||||
|
||||
result := F.Pipe1(
|
||||
Of[testState](5),
|
||||
Chain[testState](func(x int) StateReaderIOResult[testState, string] {
|
||||
Chain(func(x int) StateReaderIOResult[testState, string] {
|
||||
return Of[testState](fmt.Sprintf("value: %d", x))
|
||||
}),
|
||||
)
|
||||
@@ -210,12 +210,12 @@ func TestFromState(t *testing.T) {
|
||||
return P.MakePair(newState, newState.counter)
|
||||
}
|
||||
|
||||
result := FromState[testState](stateComp)
|
||||
result := FromState(stateComp)
|
||||
res := result(initialState)(ctx)()
|
||||
|
||||
assert.True(t, RES.IsRight(res))
|
||||
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
|
||||
assert.Equal(t, 11, P.Tail(p)) // Incremented value
|
||||
assert.Equal(t, 11, P.Tail(p)) // Incremented value
|
||||
assert.Equal(t, 11, P.Head(p).counter) // State updated
|
||||
return p
|
||||
})(res)
|
||||
@@ -257,7 +257,7 @@ func TestLocal(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), "key", "value1")
|
||||
|
||||
// Create a computation that uses the context
|
||||
comp := Asks[testState, string](func(c context.Context) StateReaderIOResult[testState, string] {
|
||||
comp := Asks(func(c context.Context) StateReaderIOResult[testState, string] {
|
||||
val := c.Value("key").(string)
|
||||
return Of[testState](val)
|
||||
})
|
||||
@@ -281,7 +281,7 @@ func TestAsks(t *testing.T) {
|
||||
state := testState{counter: 0}
|
||||
ctx := context.WithValue(context.Background(), "multiplier", 7)
|
||||
|
||||
result := Asks[testState, int](func(c context.Context) StateReaderIOResult[testState, int] {
|
||||
result := Asks(func(c context.Context) StateReaderIOResult[testState, int] {
|
||||
mult := c.Value("multiplier").(int)
|
||||
return Of[testState](mult * 5)
|
||||
})
|
||||
@@ -461,19 +461,19 @@ func TestStatefulComputation(t *testing.T) {
|
||||
|
||||
// Chain multiple stateful operations
|
||||
result := F.Pipe2(
|
||||
FromState[testState](incrementAndGet),
|
||||
Chain[testState](func(v1 int) StateReaderIOResult[testState, int] {
|
||||
return FromState[testState](incrementAndGet)
|
||||
FromState(incrementAndGet),
|
||||
Chain(func(v1 int) StateReaderIOResult[testState, int] {
|
||||
return FromState(incrementAndGet)
|
||||
}),
|
||||
Chain[testState](func(v2 int) StateReaderIOResult[testState, int] {
|
||||
return FromState[testState](incrementAndGet)
|
||||
Chain(func(v2 int) StateReaderIOResult[testState, int] {
|
||||
return FromState(incrementAndGet)
|
||||
}),
|
||||
)
|
||||
|
||||
res := result(initialState)(ctx)()
|
||||
assert.True(t, RES.IsRight(res))
|
||||
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
|
||||
assert.Equal(t, 3, P.Tail(p)) // Last incremented value
|
||||
assert.Equal(t, 3, P.Tail(p)) // Last incremented value
|
||||
assert.Equal(t, 3, P.Head(p).counter) // State updated three times
|
||||
return p
|
||||
})(res)
|
||||
@@ -488,7 +488,7 @@ func TestErrorPropagation(t *testing.T) {
|
||||
// Chain operations where the second one fails
|
||||
result := F.Pipe1(
|
||||
Of[testState](42),
|
||||
Chain[testState](func(x int) StateReaderIOResult[testState, int] {
|
||||
Chain(func(x int) StateReaderIOResult[testState, int] {
|
||||
return Left[testState, int](testErr)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -19,12 +19,12 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
RIORES "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
ST "github.com/IBM/fp-go/v2/context/statereaderioresult"
|
||||
EQ "github.com/IBM/fp-go/v2/eq"
|
||||
L "github.com/IBM/fp-go/v2/internal/monad/testing"
|
||||
P "github.com/IBM/fp-go/v2/pair"
|
||||
RES "github.com/IBM/fp-go/v2/result"
|
||||
RIORES "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
ST "github.com/IBM/fp-go/v2/context/statereaderioresult"
|
||||
)
|
||||
|
||||
// AssertLaws asserts the monad laws for the StateReaderIOResult monad
|
||||
|
||||
67
v2/coverage.txt
Normal file
67
v2/coverage.txt
Normal file
@@ -0,0 +1,67 @@
|
||||
mode: set
|
||||
github.com/IBM/fp-go/v2/readerresult/array.go:33.74,35.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerresult/array.go:49.98,51.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerresult/array.go:68.76,70.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/bind.go:42.22,44.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/bind.go:93.49,95.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/bind.go:103.49,105.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerresult/bind.go:113.49,115.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerresult/bind.go:122.22,124.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerresult/bind.go:172.49,174.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/bind.go:211.21,213.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerresult/bind.go:252.21,254.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerresult/bind.go:288.21,290.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerresult/bind.go:321.21,323.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerresult/curry.go:35.64,37.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/curry.go:46.81,48.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/curry.go:58.98,60.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/curry.go:69.115,71.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/curry.go:81.83,83.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/curry.go:92.100,94.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/curry.go:103.117,105.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/from.go:33.70,35.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/from.go:45.80,47.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/from.go:57.92,59.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/from.go:69.104,71.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/monoid.go:37.62,45.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/monoid.go:64.70,69.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/monoid.go:91.62,98.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:41.59,43.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:49.59,51.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:61.63,63.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:73.66,75.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:85.49,87.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:98.46,100.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:106.62,108.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:120.83,122.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:133.54,135.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:147.92,149.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:160.63,162.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:173.43,175.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:189.101,191.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:197.71,199.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:215.89,217.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:234.131,236.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:249.100,251.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:265.70,267.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:282.81,289.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:303.38,305.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:317.56,319.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:330.103,337.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:348.74,354.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:367.97,369.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:381.84,383.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:395.108,397.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:409.79,411.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:426.88,428.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:440.61,442.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:453.85,455.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:460.55,462.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:473.94,475.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:486.65,488.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:494.103,502.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/reader.go:508.71,515.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerresult/sequence.go:35.78,40.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/sequence.go:54.35,60.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/sequence.go:75.38,82.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerresult/sequence.go:95.41,103.2 1 1
|
||||
@@ -95,11 +95,11 @@ func (o *eitherMonad[E, A, B]) Chain(f Kleisli[E, A, B]) Operator[E, A, B] {
|
||||
// m := either.Monad[error, int, int]()
|
||||
//
|
||||
// // Map transforms the value
|
||||
// value := m.Map(func(x int) int { return x * 2 })(either.Right[error](21))
|
||||
// value := m.Map(N.Mul(2))(either.Right[error](21))
|
||||
// // value is Right(42)
|
||||
//
|
||||
// // Ap applies wrapped functions (also fails fast)
|
||||
// fn := either.Right[error](func(x int) int { return x + 1 })
|
||||
// fn := either.Right[error](N.Add(1))
|
||||
// result := m.Ap(value)(fn)
|
||||
// // result is Right(43)
|
||||
//
|
||||
|
||||
@@ -137,7 +137,7 @@ func MonadApV[B, A, E any](sg S.Semigroup[E]) func(fab Either[E, func(a A) B], f
|
||||
//
|
||||
//go:inline
|
||||
func ApV[B, A, E any](sg S.Semigroup[E]) func(Either[E, A]) Operator[E, func(A) B, B] {
|
||||
apv := MonadApV[B, A, E](sg)
|
||||
apv := MonadApV[B, A](sg)
|
||||
return func(e Either[E, A]) Operator[E, func(A) B, B] {
|
||||
return F.Bind2nd(apv, e)
|
||||
}
|
||||
|
||||
406
v2/endomorphism/builder.go
Normal file
406
v2/endomorphism/builder.go
Normal file
@@ -0,0 +1,406 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package endomorphism
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
A "github.com/IBM/fp-go/v2/internal/array"
|
||||
)
|
||||
|
||||
// Build applies an endomorphism to the zero value of type A, effectively using
|
||||
// the endomorphism as a builder pattern.
|
||||
//
|
||||
// # Endomorphism as Builder Pattern
|
||||
//
|
||||
// An endomorphism (a function from type A to type A) can be viewed as a builder pattern
|
||||
// because it transforms a value of a type into another value of the same type. When you
|
||||
// compose multiple endomorphisms together, you create a pipeline of transformations that
|
||||
// build up a final value step by step.
|
||||
//
|
||||
// The Build function starts with the zero value of type A and applies the endomorphism
|
||||
// to it, making it particularly useful for building complex values from scratch using
|
||||
// a functional composition of transformations.
|
||||
//
|
||||
// # Builder Pattern Characteristics
|
||||
//
|
||||
// Traditional builder patterns have these characteristics:
|
||||
// 1. Start with an initial (often empty) state
|
||||
// 2. Apply a series of transformations/configurations
|
||||
// 3. Return the final built object
|
||||
//
|
||||
// Endomorphisms provide the same pattern functionally:
|
||||
// 1. Start with zero value: var a A
|
||||
// 2. Apply composed endomorphisms: e(a)
|
||||
// 3. Return the transformed value
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The type being built/transformed
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - e: An endomorphism (or composition of endomorphisms) that transforms type A
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// The result of applying the endomorphism to the zero value of type A
|
||||
//
|
||||
// # Example - Building a Configuration Object
|
||||
//
|
||||
// type Config struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// Timeout time.Duration
|
||||
// Debug bool
|
||||
// }
|
||||
//
|
||||
// // Define builder functions as endomorphisms
|
||||
// withHost := func(host string) Endomorphism[Config] {
|
||||
// return func(c Config) Config {
|
||||
// c.Host = host
|
||||
// return c
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// withPort := func(port int) Endomorphism[Config] {
|
||||
// return func(c Config) Config {
|
||||
// c.Port = port
|
||||
// return c
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// withTimeout := func(d time.Duration) Endomorphism[Config] {
|
||||
// return func(c Config) Config {
|
||||
// c.Timeout = d
|
||||
// return c
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// withDebug := func(debug bool) Endomorphism[Config] {
|
||||
// return func(c Config) Config {
|
||||
// c.Debug = debug
|
||||
// return c
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Compose builders using monoid operations
|
||||
// import M "github.com/IBM/fp-go/v2/monoid"
|
||||
//
|
||||
// configBuilder := M.ConcatAll(Monoid[Config]())(
|
||||
// withHost("localhost"),
|
||||
// withPort(8080),
|
||||
// withTimeout(30 * time.Second),
|
||||
// withDebug(true),
|
||||
// )
|
||||
//
|
||||
// // Build the final configuration
|
||||
// config := Build(configBuilder)
|
||||
// // Result: Config{Host: "localhost", Port: 8080, Timeout: 30s, Debug: true}
|
||||
//
|
||||
// # Example - Building a String with Transformations
|
||||
//
|
||||
// import (
|
||||
// "strings"
|
||||
// M "github.com/IBM/fp-go/v2/monoid"
|
||||
// )
|
||||
//
|
||||
// // Define string transformation endomorphisms
|
||||
// appendHello := func(s string) string { return s + "Hello" }
|
||||
// appendSpace := func(s string) string { return s + " " }
|
||||
// appendWorld := func(s string) string { return s + "World" }
|
||||
// toUpper := strings.ToUpper
|
||||
//
|
||||
// // Compose transformations
|
||||
// stringBuilder := M.ConcatAll(Monoid[string]())(
|
||||
// appendHello,
|
||||
// appendSpace,
|
||||
// appendWorld,
|
||||
// toUpper,
|
||||
// )
|
||||
//
|
||||
// // Build the final string from empty string
|
||||
// result := Build(stringBuilder)
|
||||
// // Result: "HELLO WORLD"
|
||||
//
|
||||
// # Example - Building a Slice with Operations
|
||||
//
|
||||
// type IntSlice []int
|
||||
//
|
||||
// appendValue := func(v int) Endomorphism[IntSlice] {
|
||||
// return func(s IntSlice) IntSlice {
|
||||
// return append(s, v)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// sortSlice := func(s IntSlice) IntSlice {
|
||||
// sorted := make(IntSlice, len(s))
|
||||
// copy(sorted, s)
|
||||
// sort.Ints(sorted)
|
||||
// return sorted
|
||||
// }
|
||||
//
|
||||
// // Build a sorted slice
|
||||
// sliceBuilder := M.ConcatAll(Monoid[IntSlice]())(
|
||||
// appendValue(5),
|
||||
// appendValue(2),
|
||||
// appendValue(8),
|
||||
// appendValue(1),
|
||||
// sortSlice,
|
||||
// )
|
||||
//
|
||||
// result := Build(sliceBuilder)
|
||||
// // Result: IntSlice{1, 2, 5, 8}
|
||||
//
|
||||
// # Advantages of Endomorphism Builder Pattern
|
||||
//
|
||||
// 1. **Composability**: Builders can be composed using monoid operations
|
||||
// 2. **Immutability**: Each transformation returns a new value (if implemented immutably)
|
||||
// 3. **Type Safety**: The type system ensures all transformations work on the same type
|
||||
// 4. **Reusability**: Individual builder functions can be reused and combined differently
|
||||
// 5. **Testability**: Each transformation can be tested independently
|
||||
// 6. **Declarative**: The composition clearly expresses the building process
|
||||
//
|
||||
// # Comparison with Traditional Builder Pattern
|
||||
//
|
||||
// Traditional OOP Builder:
|
||||
//
|
||||
// config := NewConfigBuilder().
|
||||
// WithHost("localhost").
|
||||
// WithPort(8080).
|
||||
// WithTimeout(30 * time.Second).
|
||||
// Build()
|
||||
//
|
||||
// Endomorphism Builder:
|
||||
//
|
||||
// config := Build(M.ConcatAll(Monoid[Config]())(
|
||||
// withHost("localhost"),
|
||||
// withPort(8080),
|
||||
// withTimeout(30 * time.Second),
|
||||
// ))
|
||||
//
|
||||
// Both achieve the same goal, but the endomorphism approach:
|
||||
// - Uses pure functions instead of methods
|
||||
// - Leverages algebraic properties (monoid) for composition
|
||||
// - Allows for more flexible composition patterns
|
||||
// - Integrates naturally with other functional programming constructs
|
||||
func Build[A any](e Endomorphism[A]) A {
|
||||
var a A
|
||||
return e(a)
|
||||
}
|
||||
|
||||
// ConcatAll combines multiple endomorphisms into a single endomorphism using composition.
|
||||
//
|
||||
// This function takes a slice of endomorphisms and combines them using the monoid's
|
||||
// concat operation (which is composition). The resulting endomorphism, when applied,
|
||||
// will execute all the input endomorphisms in RIGHT-TO-LEFT order (mathematical composition order).
|
||||
//
|
||||
// IMPORTANT: Execution order is RIGHT-TO-LEFT:
|
||||
// - ConcatAll([]Endomorphism{f, g, h}) creates an endomorphism that applies h, then g, then f
|
||||
// - This is equivalent to f ∘ g ∘ h in mathematical notation
|
||||
// - The last endomorphism in the slice is applied first
|
||||
//
|
||||
// If the slice is empty, returns the identity endomorphism.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - T: The type that the endomorphisms operate on
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - es: A slice of endomorphisms to combine
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A single endomorphism that represents the composition of all input endomorphisms
|
||||
//
|
||||
// # Example - Basic Composition
|
||||
//
|
||||
// double := N.Mul(2)
|
||||
// increment := N.Add(1)
|
||||
// square := func(x int) int { return x * x }
|
||||
//
|
||||
// // Combine endomorphisms (RIGHT-TO-LEFT execution)
|
||||
// combined := ConcatAll([]Endomorphism[int]{double, increment, square})
|
||||
// result := combined(5)
|
||||
// // Execution: square(5) = 25, increment(25) = 26, double(26) = 52
|
||||
// // Result: 52
|
||||
//
|
||||
// # Example - Building with ConcatAll
|
||||
//
|
||||
// type Config struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
//
|
||||
// withHost := func(host string) Endomorphism[Config] {
|
||||
// return func(c Config) Config {
|
||||
// c.Host = host
|
||||
// return c
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// withPort := func(port int) Endomorphism[Config] {
|
||||
// return func(c Config) Config {
|
||||
// c.Port = port
|
||||
// return c
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Combine configuration builders
|
||||
// configBuilder := ConcatAll([]Endomorphism[Config]{
|
||||
// withHost("localhost"),
|
||||
// withPort(8080),
|
||||
// })
|
||||
//
|
||||
// // Apply to zero value
|
||||
// config := Build(configBuilder)
|
||||
// // Result: Config{Host: "localhost", Port: 8080}
|
||||
//
|
||||
// # Example - Empty Slice
|
||||
//
|
||||
// // Empty slice returns identity
|
||||
// identity := ConcatAll([]Endomorphism[int]{})
|
||||
// result := identity(42) // Returns: 42
|
||||
//
|
||||
// # Relationship to Monoid
|
||||
//
|
||||
// ConcatAll is equivalent to using M.ConcatAll with the endomorphism Monoid:
|
||||
//
|
||||
// import M "github.com/IBM/fp-go/v2/monoid"
|
||||
//
|
||||
// // These are equivalent:
|
||||
// result1 := ConcatAll(endomorphisms)
|
||||
// result2 := M.ConcatAll(Monoid[T]())(endomorphisms)
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// 1. **Pipeline Construction**: Build transformation pipelines from individual steps
|
||||
// 2. **Configuration Building**: Combine multiple configuration setters
|
||||
// 3. **Data Transformation**: Chain multiple data transformations
|
||||
// 4. **Middleware Composition**: Combine middleware functions
|
||||
// 5. **Validation Chains**: Compose multiple validation functions
|
||||
func ConcatAll[T any](es []Endomorphism[T]) Endomorphism[T] {
|
||||
return A.Reduce(es, MonadCompose[T], function.Identity[T])
|
||||
}
|
||||
|
||||
// Reduce applies a slice of endomorphisms to the zero value of type T in LEFT-TO-RIGHT order.
|
||||
//
|
||||
// This function is a convenience wrapper that:
|
||||
// 1. Starts with the zero value of type T
|
||||
// 2. Applies each endomorphism in the slice from left to right
|
||||
// 3. Returns the final transformed value
|
||||
//
|
||||
// IMPORTANT: Execution order is LEFT-TO-RIGHT:
|
||||
// - Reduce([]Endomorphism{f, g, h}) applies f first, then g, then h
|
||||
// - This is the opposite of ConcatAll's RIGHT-TO-LEFT order
|
||||
// - Each endomorphism receives the result of the previous one
|
||||
//
|
||||
// This is equivalent to: Build(ConcatAll(reverse(es))) but more efficient and clearer
|
||||
// for left-to-right sequential application.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - T: The type being transformed
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - es: A slice of endomorphisms to apply sequentially
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// The final value after applying all endomorphisms to the zero value
|
||||
//
|
||||
// # Example - Sequential Transformations
|
||||
//
|
||||
// double := N.Mul(2)
|
||||
// increment := N.Add(1)
|
||||
// square := func(x int) int { return x * x }
|
||||
//
|
||||
// // Apply transformations LEFT-TO-RIGHT
|
||||
// result := Reduce([]Endomorphism[int]{double, increment, square})
|
||||
// // Execution: 0 -> double(0) = 0 -> increment(0) = 1 -> square(1) = 1
|
||||
// // Result: 1
|
||||
//
|
||||
// // With a non-zero starting point, use a custom initial value:
|
||||
// addTen := N.Add(10)
|
||||
// result2 := Reduce([]Endomorphism[int]{addTen, double, increment})
|
||||
// // Execution: 0 -> addTen(0) = 10 -> double(10) = 20 -> increment(20) = 21
|
||||
// // Result: 21
|
||||
//
|
||||
// # Example - Building a String
|
||||
//
|
||||
// appendHello := func(s string) string { return s + "Hello" }
|
||||
// appendSpace := func(s string) string { return s + " " }
|
||||
// appendWorld := func(s string) string { return s + "World" }
|
||||
//
|
||||
// // Build string LEFT-TO-RIGHT
|
||||
// result := Reduce([]Endomorphism[string]{
|
||||
// appendHello,
|
||||
// appendSpace,
|
||||
// appendWorld,
|
||||
// })
|
||||
// // Execution: "" -> "Hello" -> "Hello " -> "Hello World"
|
||||
// // Result: "Hello World"
|
||||
//
|
||||
// # Example - Configuration Building
|
||||
//
|
||||
// type Settings struct {
|
||||
// Theme string
|
||||
// FontSize int
|
||||
// }
|
||||
//
|
||||
// withTheme := func(theme string) Endomorphism[Settings] {
|
||||
// return func(s Settings) Settings {
|
||||
// s.Theme = theme
|
||||
// return s
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// withFontSize := func(size int) Endomorphism[Settings] {
|
||||
// return func(s Settings) Settings {
|
||||
// s.FontSize = size
|
||||
// return s
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Build settings LEFT-TO-RIGHT
|
||||
// settings := Reduce([]Endomorphism[Settings]{
|
||||
// withTheme("dark"),
|
||||
// withFontSize(14),
|
||||
// })
|
||||
// // Result: Settings{Theme: "dark", FontSize: 14}
|
||||
//
|
||||
// # Comparison with ConcatAll
|
||||
//
|
||||
// // ConcatAll: RIGHT-TO-LEFT composition, returns endomorphism
|
||||
// endo := ConcatAll([]Endomorphism[int]{f, g, h})
|
||||
// result1 := endo(value) // Applies h, then g, then f
|
||||
//
|
||||
// // Reduce: LEFT-TO-RIGHT application, returns final value
|
||||
// result2 := Reduce([]Endomorphism[int]{f, g, h})
|
||||
// // Applies f to zero, then g, then h
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// 1. **Sequential Processing**: Apply transformations in order
|
||||
// 2. **Pipeline Execution**: Execute a pipeline from start to finish
|
||||
// 3. **Builder Pattern**: Build objects step by step
|
||||
// 4. **State Machines**: Apply state transitions in sequence
|
||||
// 5. **Data Flow**: Transform data through multiple stages
|
||||
func Reduce[T any](es []Endomorphism[T]) T {
|
||||
var t T
|
||||
return A.Reduce(es, func(t T, e Endomorphism[T]) T { return e(t) }, t)
|
||||
}
|
||||
254
v2/endomorphism/builder_example_test.go
Normal file
254
v2/endomorphism/builder_example_test.go
Normal file
@@ -0,0 +1,254 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package endomorphism_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
// Example_build_basicUsage demonstrates basic usage of the Build function
|
||||
// to construct a value from the zero value using endomorphisms.
|
||||
func Example_build_basicUsage() {
|
||||
// Define simple endomorphisms
|
||||
addTen := N.Add(10)
|
||||
double := N.Mul(2)
|
||||
|
||||
// Compose them using monoid (RIGHT-TO-LEFT execution)
|
||||
// double is applied first, then addTen
|
||||
builder := M.ConcatAll(endomorphism.Monoid[int]())(A.From(
|
||||
addTen,
|
||||
double,
|
||||
))
|
||||
|
||||
// Build from zero value: 0 * 2 = 0, 0 + 10 = 10
|
||||
result := endomorphism.Build(builder)
|
||||
fmt.Println(result)
|
||||
// Output: 10
|
||||
}
|
||||
|
||||
// Example_build_configBuilder demonstrates using Build as a configuration builder pattern.
|
||||
func Example_build_configBuilder() {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
Timeout time.Duration
|
||||
Debug bool
|
||||
}
|
||||
|
||||
// Define builder functions as endomorphisms
|
||||
withHost := func(host string) endomorphism.Endomorphism[Config] {
|
||||
return func(c Config) Config {
|
||||
c.Host = host
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
withPort := func(port int) endomorphism.Endomorphism[Config] {
|
||||
return func(c Config) Config {
|
||||
c.Port = port
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
withTimeout := func(d time.Duration) endomorphism.Endomorphism[Config] {
|
||||
return func(c Config) Config {
|
||||
c.Timeout = d
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
withDebug := func(debug bool) endomorphism.Endomorphism[Config] {
|
||||
return func(c Config) Config {
|
||||
c.Debug = debug
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
// Compose builders using monoid
|
||||
configBuilder := M.ConcatAll(endomorphism.Monoid[Config]())([]endomorphism.Endomorphism[Config]{
|
||||
withHost("localhost"),
|
||||
withPort(8080),
|
||||
withTimeout(30 * time.Second),
|
||||
withDebug(true),
|
||||
})
|
||||
|
||||
// Build the configuration from zero value
|
||||
config := endomorphism.Build(configBuilder)
|
||||
|
||||
fmt.Printf("Host: %s\n", config.Host)
|
||||
fmt.Printf("Port: %d\n", config.Port)
|
||||
fmt.Printf("Timeout: %v\n", config.Timeout)
|
||||
fmt.Printf("Debug: %v\n", config.Debug)
|
||||
// Output:
|
||||
// Host: localhost
|
||||
// Port: 8080
|
||||
// Timeout: 30s
|
||||
// Debug: true
|
||||
}
|
||||
|
||||
// Example_build_stringBuilder demonstrates building a string using endomorphisms.
|
||||
func Example_build_stringBuilder() {
|
||||
// Define string transformation endomorphisms
|
||||
appendHello := func(s string) string { return s + "Hello" }
|
||||
appendSpace := func(s string) string { return s + " " }
|
||||
appendWorld := func(s string) string { return s + "World" }
|
||||
appendExclamation := func(s string) string { return s + "!" }
|
||||
|
||||
// Compose transformations (RIGHT-TO-LEFT execution)
|
||||
stringBuilder := M.ConcatAll(endomorphism.Monoid[string]())([]endomorphism.Endomorphism[string]{
|
||||
appendHello,
|
||||
appendSpace,
|
||||
appendWorld,
|
||||
appendExclamation,
|
||||
})
|
||||
|
||||
// Build the string from empty string
|
||||
result := endomorphism.Build(stringBuilder)
|
||||
fmt.Println(result)
|
||||
// Output: !World Hello
|
||||
}
|
||||
|
||||
// Example_build_personBuilder demonstrates building a complex struct using the builder pattern.
|
||||
func Example_build_personBuilder() {
|
||||
type Person struct {
|
||||
FirstName string
|
||||
LastName string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
// Define builder functions
|
||||
withFirstName := func(name string) endomorphism.Endomorphism[Person] {
|
||||
return func(p Person) Person {
|
||||
p.FirstName = name
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
withLastName := func(name string) endomorphism.Endomorphism[Person] {
|
||||
return func(p Person) Person {
|
||||
p.LastName = name
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
withAge := func(age int) endomorphism.Endomorphism[Person] {
|
||||
return func(p Person) Person {
|
||||
p.Age = age
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
withEmail := func(email string) endomorphism.Endomorphism[Person] {
|
||||
return func(p Person) Person {
|
||||
p.Email = email
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
// Build a person
|
||||
personBuilder := M.ConcatAll(endomorphism.Monoid[Person]())([]endomorphism.Endomorphism[Person]{
|
||||
withFirstName("Alice"),
|
||||
withLastName("Smith"),
|
||||
withAge(30),
|
||||
withEmail("alice.smith@example.com"),
|
||||
})
|
||||
|
||||
person := endomorphism.Build(personBuilder)
|
||||
|
||||
fmt.Printf("%s %s, Age: %d, Email: %s\n",
|
||||
person.FirstName, person.LastName, person.Age, person.Email)
|
||||
// Output: Alice Smith, Age: 30, Email: alice.smith@example.com
|
||||
}
|
||||
|
||||
// Example_build_conditionalBuilder demonstrates conditional building using endomorphisms.
|
||||
func Example_build_conditionalBuilder() {
|
||||
type Settings struct {
|
||||
Theme string
|
||||
FontSize int
|
||||
AutoSave bool
|
||||
Animations bool
|
||||
}
|
||||
|
||||
withTheme := func(theme string) endomorphism.Endomorphism[Settings] {
|
||||
return func(s Settings) Settings {
|
||||
s.Theme = theme
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
withFontSize := func(size int) endomorphism.Endomorphism[Settings] {
|
||||
return func(s Settings) Settings {
|
||||
s.FontSize = size
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
withAutoSave := func(enabled bool) endomorphism.Endomorphism[Settings] {
|
||||
return func(s Settings) Settings {
|
||||
s.AutoSave = enabled
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
withAnimations := func(enabled bool) endomorphism.Endomorphism[Settings] {
|
||||
return func(s Settings) Settings {
|
||||
s.Animations = enabled
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
// Build settings conditionally
|
||||
isDarkMode := true
|
||||
isAccessibilityMode := true
|
||||
|
||||
// Note: Monoid executes RIGHT-TO-LEFT, so later items in the slice are applied first
|
||||
// We need to add items in reverse order for the desired effect
|
||||
builders := []endomorphism.Endomorphism[Settings]{}
|
||||
|
||||
if isAccessibilityMode {
|
||||
builders = append(builders, withFontSize(18)) // Will be applied last (overrides)
|
||||
builders = append(builders, withAnimations(false))
|
||||
}
|
||||
|
||||
if isDarkMode {
|
||||
builders = append(builders, withTheme("dark"))
|
||||
} else {
|
||||
builders = append(builders, withTheme("light"))
|
||||
}
|
||||
|
||||
builders = append(builders, withAutoSave(true))
|
||||
builders = append(builders, withFontSize(14)) // Will be applied first
|
||||
|
||||
settingsBuilder := M.ConcatAll(endomorphism.Monoid[Settings]())(builders)
|
||||
settings := endomorphism.Build(settingsBuilder)
|
||||
|
||||
fmt.Printf("Theme: %s\n", settings.Theme)
|
||||
fmt.Printf("FontSize: %d\n", settings.FontSize)
|
||||
fmt.Printf("AutoSave: %v\n", settings.AutoSave)
|
||||
fmt.Printf("Animations: %v\n", settings.Animations)
|
||||
// Output:
|
||||
// Theme: dark
|
||||
// FontSize: 18
|
||||
// AutoSave: true
|
||||
// Animations: false
|
||||
}
|
||||
@@ -37,7 +37,7 @@
|
||||
//
|
||||
// // Define some endomorphisms
|
||||
// double := N.Mul(2)
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// increment := N.Add(1)
|
||||
//
|
||||
// // Compose them (RIGHT-TO-LEFT execution)
|
||||
// composed := endomorphism.Compose(double, increment)
|
||||
@@ -63,8 +63,8 @@
|
||||
// // Combine multiple endomorphisms (RIGHT-TO-LEFT execution)
|
||||
// combined := M.ConcatAll(monoid)(
|
||||
// N.Mul(2), // applied third
|
||||
// func(x int) int { return x + 1 }, // applied second
|
||||
// func(x int) int { return x * 3 }, // applied first
|
||||
// N.Add(1), // applied second
|
||||
// N.Mul(3), // applied first
|
||||
// )
|
||||
// result := combined(5) // (5 * 3) = 15, (15 + 1) = 16, (16 * 2) = 32
|
||||
//
|
||||
@@ -75,7 +75,7 @@
|
||||
//
|
||||
// // Chain allows sequencing of endomorphisms (LEFT-TO-RIGHT)
|
||||
// f := N.Mul(2)
|
||||
// g := func(x int) int { return x + 1 }
|
||||
// g := N.Add(1)
|
||||
// chained := endomorphism.MonadChain(f, g) // f first, then g
|
||||
// result := chained(5) // (5 * 2) + 1 = 11
|
||||
//
|
||||
@@ -84,7 +84,7 @@
|
||||
// The key difference between Compose and Chain/MonadChain is execution order:
|
||||
//
|
||||
// double := N.Mul(2)
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// increment := N.Add(1)
|
||||
//
|
||||
// // Compose: RIGHT-TO-LEFT (mathematical composition)
|
||||
// composed := endomorphism.Compose(double, increment)
|
||||
|
||||
@@ -38,7 +38,7 @@ import (
|
||||
// Example:
|
||||
//
|
||||
// double := N.Mul(2)
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// increment := N.Add(1)
|
||||
// result := endomorphism.MonadAp(double, increment) // Composes: double ∘ increment
|
||||
// // result(5) = double(increment(5)) = double(6) = 12
|
||||
func MonadAp[A any](fab Endomorphism[A], fa Endomorphism[A]) Endomorphism[A] {
|
||||
@@ -62,7 +62,7 @@ func MonadAp[A any](fab Endomorphism[A], fa Endomorphism[A]) Endomorphism[A] {
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// increment := N.Add(1)
|
||||
// applyIncrement := endomorphism.Ap(increment)
|
||||
// double := N.Mul(2)
|
||||
// composed := applyIncrement(double) // double ∘ increment
|
||||
@@ -92,7 +92,7 @@ func Ap[A any](fa Endomorphism[A]) Operator[A] {
|
||||
// Example:
|
||||
//
|
||||
// double := N.Mul(2)
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// increment := N.Add(1)
|
||||
//
|
||||
// // MonadCompose executes RIGHT-TO-LEFT: increment first, then double
|
||||
// composed := endomorphism.MonadCompose(double, increment)
|
||||
@@ -124,7 +124,7 @@ func MonadCompose[A any](f, g Endomorphism[A]) Endomorphism[A] {
|
||||
// Example:
|
||||
//
|
||||
// double := N.Mul(2)
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// increment := N.Add(1)
|
||||
// mapped := endomorphism.MonadMap(double, increment)
|
||||
// // mapped(5) = double(increment(5)) = double(6) = 12
|
||||
func MonadMap[A any](f, g Endomorphism[A]) Endomorphism[A] {
|
||||
@@ -151,7 +151,7 @@ func MonadMap[A any](f, g Endomorphism[A]) Endomorphism[A] {
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// increment := N.Add(1)
|
||||
// composeWithIncrement := endomorphism.Compose(increment)
|
||||
// double := N.Mul(2)
|
||||
//
|
||||
@@ -188,7 +188,7 @@ func Compose[A any](g Endomorphism[A]) Operator[A] {
|
||||
//
|
||||
// double := N.Mul(2)
|
||||
// mapDouble := endomorphism.Map(double)
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// increment := N.Add(1)
|
||||
// mapped := mapDouble(increment)
|
||||
// // mapped(5) = double(increment(5)) = double(6) = 12
|
||||
func Map[A any](f Endomorphism[A]) Operator[A] {
|
||||
@@ -216,7 +216,7 @@ func Map[A any](f Endomorphism[A]) Operator[A] {
|
||||
// Example:
|
||||
//
|
||||
// double := N.Mul(2)
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// increment := N.Add(1)
|
||||
//
|
||||
// // MonadChain executes LEFT-TO-RIGHT: double first, then increment
|
||||
// chained := endomorphism.MonadChain(double, increment)
|
||||
@@ -294,7 +294,7 @@ func ChainFirst[A any](f Endomorphism[A]) Operator[A] {
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// increment := N.Add(1)
|
||||
// chainWithIncrement := endomorphism.Chain(increment)
|
||||
// double := N.Mul(2)
|
||||
//
|
||||
|
||||
@@ -206,7 +206,7 @@ func TestCompose(t *testing.T) {
|
||||
// TestMonadComposeVsCompose demonstrates the relationship between MonadCompose and Compose
|
||||
func TestMonadComposeVsCompose(t *testing.T) {
|
||||
double := N.Mul(2)
|
||||
increment := func(x int) int { return x + 1 }
|
||||
increment := N.Add(1)
|
||||
|
||||
// MonadCompose takes both functions at once
|
||||
monadComposed := MonadCompose(double, increment)
|
||||
@@ -458,7 +458,7 @@ func BenchmarkCompose(b *testing.B) {
|
||||
// TestComposeVsChain demonstrates the key difference between Compose and Chain
|
||||
func TestComposeVsChain(t *testing.T) {
|
||||
double := N.Mul(2)
|
||||
increment := func(x int) int { return x + 1 }
|
||||
increment := N.Add(1)
|
||||
|
||||
// Compose executes RIGHT-TO-LEFT
|
||||
// Compose(double, increment) means: increment first, then double
|
||||
@@ -722,3 +722,352 @@ func TestChainFirst(t *testing.T) {
|
||||
// But side effect should have been executed with double's result
|
||||
assert.Equal(t, 10, sideEffect, "ChainFirst should execute second function for effect")
|
||||
}
|
||||
|
||||
// TestBuild tests the Build function
|
||||
func TestBuild(t *testing.T) {
|
||||
t.Run("build with single transformation", func(t *testing.T) {
|
||||
// Build applies endomorphism to zero value
|
||||
result := Build(double)
|
||||
assert.Equal(t, 0, result, "Build(double) on zero value should be 0")
|
||||
})
|
||||
|
||||
t.Run("build with composed transformations", func(t *testing.T) {
|
||||
// Create a builder that starts from zero and applies transformations
|
||||
builder := M.ConcatAll(Monoid[int]())([]Endomorphism[int]{
|
||||
N.Add(10),
|
||||
N.Mul(2),
|
||||
N.Add(5),
|
||||
})
|
||||
|
||||
result := Build(builder)
|
||||
// RIGHT-TO-LEFT: 0 + 5 = 5, 5 * 2 = 10, 10 + 10 = 20
|
||||
assert.Equal(t, 20, result, "Build should apply composed transformations to zero value")
|
||||
})
|
||||
|
||||
t.Run("build with identity", func(t *testing.T) {
|
||||
result := Build(Identity[int]())
|
||||
assert.Equal(t, 0, result, "Build(identity) should return zero value")
|
||||
})
|
||||
|
||||
t.Run("build string from empty", func(t *testing.T) {
|
||||
builder := M.ConcatAll(Monoid[string]())([]Endomorphism[string]{
|
||||
func(s string) string { return s + "Hello" },
|
||||
func(s string) string { return s + " " },
|
||||
func(s string) string { return s + "World" },
|
||||
})
|
||||
|
||||
result := Build(builder)
|
||||
// RIGHT-TO-LEFT: "" + "World" = "World", "World" + " " = "World ", "World " + "Hello" = "World Hello"
|
||||
assert.Equal(t, "World Hello", result, "Build should work with strings")
|
||||
})
|
||||
|
||||
t.Run("build struct with builder pattern", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
withHost := func(host string) Endomorphism[Config] {
|
||||
return func(c Config) Config {
|
||||
c.Host = host
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
withPort := func(port int) Endomorphism[Config] {
|
||||
return func(c Config) Config {
|
||||
c.Port = port
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
builder := M.ConcatAll(Monoid[Config]())([]Endomorphism[Config]{
|
||||
withHost("localhost"),
|
||||
withPort(8080),
|
||||
})
|
||||
|
||||
result := Build(builder)
|
||||
assert.Equal(t, "localhost", result.Host, "Build should set Host")
|
||||
assert.Equal(t, 8080, result.Port, "Build should set Port")
|
||||
})
|
||||
|
||||
t.Run("build slice with operations", func(t *testing.T) {
|
||||
type IntSlice []int
|
||||
|
||||
appendValue := func(v int) Endomorphism[IntSlice] {
|
||||
return func(s IntSlice) IntSlice {
|
||||
return append(s, v)
|
||||
}
|
||||
}
|
||||
|
||||
builder := M.ConcatAll(Monoid[IntSlice]())([]Endomorphism[IntSlice]{
|
||||
appendValue(1),
|
||||
appendValue(2),
|
||||
appendValue(3),
|
||||
})
|
||||
|
||||
result := Build(builder)
|
||||
// RIGHT-TO-LEFT: append 3, append 2, append 1
|
||||
assert.Equal(t, IntSlice{3, 2, 1}, result, "Build should construct slice")
|
||||
})
|
||||
}
|
||||
|
||||
// TestBuildAsBuilderPattern demonstrates using Build as a builder pattern
|
||||
func TestBuildAsBuilderPattern(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
Active bool
|
||||
}
|
||||
|
||||
// Define builder functions
|
||||
withName := func(name string) Endomorphism[Person] {
|
||||
return func(p Person) Person {
|
||||
p.Name = name
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
withAge := func(age int) Endomorphism[Person] {
|
||||
return func(p Person) Person {
|
||||
p.Age = age
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
withEmail := func(email string) Endomorphism[Person] {
|
||||
return func(p Person) Person {
|
||||
p.Email = email
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
withActive := func(active bool) Endomorphism[Person] {
|
||||
return func(p Person) Person {
|
||||
p.Active = active
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
// Build a person using the builder pattern
|
||||
personBuilder := M.ConcatAll(Monoid[Person]())([]Endomorphism[Person]{
|
||||
withName("Alice"),
|
||||
withAge(30),
|
||||
withEmail("alice@example.com"),
|
||||
withActive(true),
|
||||
})
|
||||
|
||||
person := Build(personBuilder)
|
||||
|
||||
assert.Equal(t, "Alice", person.Name)
|
||||
assert.Equal(t, 30, person.Age)
|
||||
assert.Equal(t, "alice@example.com", person.Email)
|
||||
assert.True(t, person.Active)
|
||||
}
|
||||
|
||||
// TestConcatAll tests the ConcatAll function
|
||||
func TestConcatAll(t *testing.T) {
|
||||
t.Run("concat all with multiple endomorphisms", func(t *testing.T) {
|
||||
// ConcatAll executes RIGHT-TO-LEFT
|
||||
combined := ConcatAll([]Endomorphism[int]{double, increment, square})
|
||||
result := combined(5)
|
||||
// RIGHT-TO-LEFT: square(5) = 25, increment(25) = 26, double(26) = 52
|
||||
assert.Equal(t, 52, result, "ConcatAll should execute right-to-left")
|
||||
})
|
||||
|
||||
t.Run("concat all with empty slice", func(t *testing.T) {
|
||||
// Empty slice should return identity
|
||||
identity := ConcatAll([]Endomorphism[int]{})
|
||||
result := identity(42)
|
||||
assert.Equal(t, 42, result, "ConcatAll with empty slice should return identity")
|
||||
})
|
||||
|
||||
t.Run("concat all with single endomorphism", func(t *testing.T) {
|
||||
combined := ConcatAll([]Endomorphism[int]{double})
|
||||
result := combined(5)
|
||||
assert.Equal(t, 10, result, "ConcatAll with single endomorphism should apply it")
|
||||
})
|
||||
|
||||
t.Run("concat all with two endomorphisms", func(t *testing.T) {
|
||||
// RIGHT-TO-LEFT: increment first, then double
|
||||
combined := ConcatAll([]Endomorphism[int]{double, increment})
|
||||
result := combined(5)
|
||||
assert.Equal(t, 12, result, "ConcatAll should execute right-to-left: (5 + 1) * 2 = 12")
|
||||
})
|
||||
|
||||
t.Run("concat all with strings", func(t *testing.T) {
|
||||
appendHello := func(s string) string { return s + "Hello" }
|
||||
appendSpace := func(s string) string { return s + " " }
|
||||
appendWorld := func(s string) string { return s + "World" }
|
||||
|
||||
// RIGHT-TO-LEFT execution
|
||||
combined := ConcatAll([]Endomorphism[string]{appendHello, appendSpace, appendWorld})
|
||||
result := combined("")
|
||||
// RIGHT-TO-LEFT: "" + "World" = "World", "World" + " " = "World ", "World " + "Hello" = "World Hello"
|
||||
assert.Equal(t, "World Hello", result, "ConcatAll should work with strings")
|
||||
})
|
||||
|
||||
t.Run("concat all for building structs", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
withHost := func(host string) Endomorphism[Config] {
|
||||
return func(c Config) Config {
|
||||
c.Host = host
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
withPort := func(port int) Endomorphism[Config] {
|
||||
return func(c Config) Config {
|
||||
c.Port = port
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
combined := ConcatAll([]Endomorphism[Config]{
|
||||
withHost("localhost"),
|
||||
withPort(8080),
|
||||
})
|
||||
|
||||
result := combined(Config{})
|
||||
assert.Equal(t, "localhost", result.Host)
|
||||
assert.Equal(t, 8080, result.Port)
|
||||
})
|
||||
|
||||
t.Run("concat all is equivalent to monoid ConcatAll", func(t *testing.T) {
|
||||
endos := []Endomorphism[int]{double, increment, square}
|
||||
|
||||
result1 := ConcatAll(endos)(5)
|
||||
result2 := M.ConcatAll(Monoid[int]())(endos)(5)
|
||||
|
||||
assert.Equal(t, result1, result2, "ConcatAll should be equivalent to M.ConcatAll(Monoid())")
|
||||
})
|
||||
}
|
||||
|
||||
// TestReduce tests the Reduce function
|
||||
func TestReduce(t *testing.T) {
|
||||
t.Run("reduce with multiple endomorphisms", func(t *testing.T) {
|
||||
// Reduce executes LEFT-TO-RIGHT starting from zero value
|
||||
result := Reduce([]Endomorphism[int]{double, increment, square})
|
||||
// LEFT-TO-RIGHT: 0 -> double(0) = 0 -> increment(0) = 1 -> square(1) = 1
|
||||
assert.Equal(t, 1, result, "Reduce should execute left-to-right from zero value")
|
||||
})
|
||||
|
||||
t.Run("reduce with empty slice", func(t *testing.T) {
|
||||
// Empty slice should return zero value
|
||||
result := Reduce([]Endomorphism[int]{})
|
||||
assert.Equal(t, 0, result, "Reduce with empty slice should return zero value")
|
||||
})
|
||||
|
||||
t.Run("reduce with single endomorphism", func(t *testing.T) {
|
||||
addTen := N.Add(10)
|
||||
result := Reduce([]Endomorphism[int]{addTen})
|
||||
// 0 + 10 = 10
|
||||
assert.Equal(t, 10, result, "Reduce with single endomorphism should apply it to zero")
|
||||
})
|
||||
|
||||
t.Run("reduce with sequential transformations", func(t *testing.T) {
|
||||
addTen := N.Add(10)
|
||||
// LEFT-TO-RIGHT: 0 -> addTen(0) = 10 -> double(10) = 20 -> increment(20) = 21
|
||||
result := Reduce([]Endomorphism[int]{addTen, double, increment})
|
||||
assert.Equal(t, 21, result, "Reduce should apply transformations left-to-right")
|
||||
})
|
||||
|
||||
t.Run("reduce with strings", func(t *testing.T) {
|
||||
appendHello := func(s string) string { return s + "Hello" }
|
||||
appendSpace := func(s string) string { return s + " " }
|
||||
appendWorld := func(s string) string { return s + "World" }
|
||||
|
||||
// LEFT-TO-RIGHT execution
|
||||
result := Reduce([]Endomorphism[string]{appendHello, appendSpace, appendWorld})
|
||||
// "" -> "Hello" -> "Hello " -> "Hello World"
|
||||
assert.Equal(t, "Hello World", result, "Reduce should work with strings left-to-right")
|
||||
})
|
||||
|
||||
t.Run("reduce for building structs", func(t *testing.T) {
|
||||
type Settings struct {
|
||||
Theme string
|
||||
FontSize int
|
||||
}
|
||||
|
||||
withTheme := func(theme string) Endomorphism[Settings] {
|
||||
return func(s Settings) Settings {
|
||||
s.Theme = theme
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
withFontSize := func(size int) Endomorphism[Settings] {
|
||||
return func(s Settings) Settings {
|
||||
s.FontSize = size
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
// LEFT-TO-RIGHT application
|
||||
result := Reduce([]Endomorphism[Settings]{
|
||||
withTheme("dark"),
|
||||
withFontSize(14),
|
||||
})
|
||||
|
||||
assert.Equal(t, "dark", result.Theme)
|
||||
assert.Equal(t, 14, result.FontSize)
|
||||
})
|
||||
|
||||
t.Run("reduce is equivalent to Build(ConcatAll(reverse))", func(t *testing.T) {
|
||||
addTen := N.Add(10)
|
||||
endos := []Endomorphism[int]{addTen, double, increment}
|
||||
|
||||
// Reduce applies left-to-right
|
||||
result1 := Reduce(endos)
|
||||
|
||||
// Reverse and use ConcatAll (which is right-to-left)
|
||||
reversed := []Endomorphism[int]{increment, double, addTen}
|
||||
result2 := Build(ConcatAll(reversed))
|
||||
|
||||
assert.Equal(t, result1, result2, "Reduce should be equivalent to Build(ConcatAll(reverse))")
|
||||
})
|
||||
}
|
||||
|
||||
// TestConcatAllVsReduce demonstrates the difference between ConcatAll and Reduce
|
||||
func TestConcatAllVsReduce(t *testing.T) {
|
||||
addTen := N.Add(10)
|
||||
|
||||
endos := []Endomorphism[int]{addTen, double, increment}
|
||||
|
||||
// ConcatAll: RIGHT-TO-LEFT composition, returns endomorphism
|
||||
concatResult := ConcatAll(endos)(5)
|
||||
// 5 -> increment(5) = 6 -> double(6) = 12 -> addTen(12) = 22
|
||||
|
||||
// Reduce: LEFT-TO-RIGHT application, returns value from zero
|
||||
reduceResult := Reduce(endos)
|
||||
// 0 -> addTen(0) = 10 -> double(10) = 20 -> increment(20) = 21
|
||||
|
||||
assert.NotEqual(t, concatResult, reduceResult, "ConcatAll and Reduce should produce different results")
|
||||
assert.Equal(t, 22, concatResult, "ConcatAll should execute right-to-left on input value")
|
||||
assert.Equal(t, 21, reduceResult, "Reduce should execute left-to-right from zero value")
|
||||
}
|
||||
|
||||
// TestReduceWithBuild demonstrates using Reduce vs Build with ConcatAll
|
||||
func TestReduceWithBuild(t *testing.T) {
|
||||
addFive := N.Add(5)
|
||||
multiplyByThree := N.Mul(3)
|
||||
|
||||
endos := []Endomorphism[int]{addFive, multiplyByThree}
|
||||
|
||||
// Reduce: LEFT-TO-RIGHT from zero
|
||||
reduceResult := Reduce(endos)
|
||||
// 0 -> addFive(0) = 5 -> multiplyByThree(5) = 15
|
||||
assert.Equal(t, 15, reduceResult)
|
||||
|
||||
// Build with ConcatAll: RIGHT-TO-LEFT from zero
|
||||
buildResult := Build(ConcatAll(endos))
|
||||
// 0 -> multiplyByThree(0) = 0 -> addFive(0) = 5
|
||||
assert.Equal(t, 5, buildResult)
|
||||
|
||||
assert.NotEqual(t, reduceResult, buildResult, "Reduce and Build(ConcatAll) produce different results due to execution order")
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ func Identity[A any]() Endomorphism[A] {
|
||||
//
|
||||
// sg := endomorphism.Semigroup[int]()
|
||||
// double := N.Mul(2)
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// increment := N.Add(1)
|
||||
//
|
||||
// // Combine using the semigroup (RIGHT-TO-LEFT execution)
|
||||
// combined := sg.Concat(double, increment)
|
||||
@@ -140,7 +140,7 @@ func Semigroup[A any]() S.Semigroup[Endomorphism[A]] {
|
||||
//
|
||||
// monoid := endomorphism.Monoid[int]()
|
||||
// double := N.Mul(2)
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// increment := N.Add(1)
|
||||
// square := func(x int) int { return x * x }
|
||||
//
|
||||
// // Combine multiple endomorphisms (RIGHT-TO-LEFT execution)
|
||||
|
||||
@@ -30,7 +30,7 @@ type (
|
||||
//
|
||||
// // Simple endomorphisms on integers
|
||||
// double := N.Mul(2)
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// increment := N.Add(1)
|
||||
//
|
||||
// // Both are endomorphisms of type Endomorphism[int]
|
||||
// var f endomorphism.Endomorphism[int] = double
|
||||
|
||||
@@ -103,4 +103,28 @@ var (
|
||||
// body := Body(fullResp)
|
||||
// content := string(body)
|
||||
Body = P.Tail[*H.Response, []byte]
|
||||
|
||||
// FromResponse creates a function that constructs a FullResponse from
|
||||
// a given *http.Response. It returns a function that takes a body byte
|
||||
// slice and combines it with the response to create a FullResponse.
|
||||
//
|
||||
// This is useful for functional composition where you want to partially
|
||||
// apply the response and later provide the body.
|
||||
//
|
||||
// Example:
|
||||
// makeFullResp := FromResponse(response)
|
||||
// fullResp := makeFullResp(bodyBytes)
|
||||
FromResponse = P.FromHead[[]byte, *H.Response]
|
||||
|
||||
// FromBody creates a function that constructs a FullResponse from
|
||||
// a given body byte slice. It returns a function that takes an
|
||||
// *http.Response and combines it with the body to create a FullResponse.
|
||||
//
|
||||
// This is useful for functional composition where you want to partially
|
||||
// apply the body and later provide the response.
|
||||
//
|
||||
// Example:
|
||||
// makeFullResp := FromBody(bodyBytes)
|
||||
// fullResp := makeFullResp(response)
|
||||
FromBody = P.FromTail[*H.Response, []byte]
|
||||
)
|
||||
|
||||
226
v2/idiomatic/REVIEW_SUMMARY.md
Normal file
226
v2/idiomatic/REVIEW_SUMMARY.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# Idiomatic Package Review Summary
|
||||
|
||||
**Date:** 2025-11-26
|
||||
**Reviewer:** Code Review Assistant
|
||||
|
||||
## Overview
|
||||
|
||||
This document summarizes the comprehensive review of the `idiomatic` package and its subpackages, including documentation fixes, additions, and test coverage analysis.
|
||||
|
||||
## Documentation Improvements
|
||||
|
||||
### 1. Main Package (`idiomatic/`)
|
||||
- ✅ **Status:** Documentation is comprehensive and well-structured
|
||||
- **File:** `doc.go` (505 lines)
|
||||
- **Quality:** Excellent - includes overview, performance comparisons, usage examples, and best practices
|
||||
|
||||
### 2. Option Package (`idiomatic/option/`)
|
||||
- ✅ **Fixed:** Added missing copyright headers to `types.go` and `function.go`
|
||||
- ✅ **Fixed:** Added comprehensive documentation for type aliases in `types.go`
|
||||
- ✅ **Fixed:** Enhanced function documentation in `function.go` with examples
|
||||
- ✅ **Fixed:** Added missing documentation for `FromZero`, `FromNonZero`, and `FromEq` functions
|
||||
- **Files Updated:**
|
||||
- `types.go` - Added copyright header and type documentation
|
||||
- `function.go` - Added copyright header and improved function docs
|
||||
- `option.go` - Enhanced documentation for utility functions
|
||||
|
||||
### 3. Result Package (`idiomatic/result/`)
|
||||
- ✅ **Fixed:** Added missing copyright header to `function.go`
|
||||
- ✅ **Fixed:** Enhanced function documentation with examples
|
||||
- **Files Updated:**
|
||||
- `function.go` - Added copyright header and improved documentation
|
||||
- `types.go` - Already had good documentation
|
||||
|
||||
### 4. IOResult Package (`idiomatic/ioresult/`)
|
||||
- ✅ **Status:** Documentation is comprehensive
|
||||
- **File:** `doc.go` (198 lines)
|
||||
- **Quality:** Excellent - includes detailed explanations of IO operations, lazy evaluation, and side effects
|
||||
|
||||
### 5. ReaderIOResult Package (`idiomatic/readerioresult/`)
|
||||
- ✅ **Created:** New `doc.go` file (96 lines)
|
||||
- ✅ **Fixed:** Added comprehensive type documentation to `types.go`
|
||||
- **New Documentation Includes:**
|
||||
- Package overview and use cases
|
||||
- Basic usage examples
|
||||
- Composition patterns
|
||||
- Error handling strategies
|
||||
- Relationship to other monads
|
||||
|
||||
### 6. ReaderResult Package (`idiomatic/readerresult/`)
|
||||
- ✅ **Fixed:** Added comprehensive type documentation to `types.go`
|
||||
- **Existing:** `doc.go` already present (178 lines) with excellent documentation
|
||||
|
||||
## Test Coverage Analysis
|
||||
|
||||
### Option Package Tests
|
||||
**File:** `idiomatic/option/option_test.go`
|
||||
|
||||
**Existing Coverage:**
|
||||
- ✅ `IsNone` - Tested
|
||||
- ✅ `IsSome` - Tested
|
||||
- ✅ `Map` - Tested
|
||||
- ✅ `Ap` - Tested
|
||||
- ✅ `Chain` - Tested
|
||||
- ✅ `ChainTo` - Comprehensive tests with multiple scenarios
|
||||
|
||||
**Missing Tests (Commented Out):**
|
||||
- ⚠️ `Flatten` - Test commented out
|
||||
- ⚠️ `Fold` - Test commented out
|
||||
- ⚠️ `FromPredicate` - Test commented out
|
||||
- ⚠️ `Alt` - Test commented out
|
||||
|
||||
**Recommendations:**
|
||||
1. Uncomment and fix the commented-out tests
|
||||
2. Add tests for:
|
||||
- `FromZero`
|
||||
- `FromNonZero`
|
||||
- `FromEq`
|
||||
- `FromNillable`
|
||||
- `MapTo`
|
||||
- `GetOrElse`
|
||||
- `ChainFirst`
|
||||
- `Reduce`
|
||||
- `Filter`
|
||||
- `Flap`
|
||||
- `ToString`
|
||||
|
||||
### Result Package Tests
|
||||
**File:** `idiomatic/result/either_test.go`
|
||||
|
||||
**Existing Coverage:**
|
||||
- ✅ `IsLeft` - Tested
|
||||
- ✅ `IsRight` - Tested
|
||||
- ✅ `Map` - Tested
|
||||
- ✅ `Ap` - Tested
|
||||
- ✅ `Alt` - Tested
|
||||
- ✅ `ChainFirst` - Tested
|
||||
- ✅ `ChainOptionK` - Tested
|
||||
- ✅ `FromOption` - Tested
|
||||
- ✅ `ToString` - Tested
|
||||
|
||||
**Missing Tests:**
|
||||
- ⚠️ `Of` - Not explicitly tested
|
||||
- ⚠️ `BiMap` - Not tested
|
||||
- ⚠️ `MapTo` - Not tested
|
||||
- ⚠️ `MapLeft` - Not tested
|
||||
- ⚠️ `Chain` - Not tested
|
||||
- ⚠️ `ChainTo` - Not tested
|
||||
- ⚠️ `ToOption` - Not tested
|
||||
- ⚠️ `FromError` - Not tested
|
||||
- ⚠️ `ToError` - Not tested
|
||||
- ⚠️ `Fold` - Not tested
|
||||
- ⚠️ `FromPredicate` - Not tested
|
||||
- ⚠️ `FromNillable` - Not tested
|
||||
- ⚠️ `GetOrElse` - Not tested
|
||||
- ⚠️ `Reduce` - Not tested
|
||||
- ⚠️ `OrElse` - Not tested
|
||||
- ⚠️ `ToType` - Not tested
|
||||
- ⚠️ `Memoize` - Not tested
|
||||
- ⚠️ `Flap` - Not tested
|
||||
|
||||
### IOResult Package Tests
|
||||
**File:** `idiomatic/ioresult/monad_test.go`
|
||||
|
||||
**Existing Coverage:** ✅ **EXCELLENT**
|
||||
- ✅ Comprehensive monad law tests (left identity, right identity, associativity)
|
||||
- ✅ Functor law tests (composition, identity)
|
||||
- ✅ Pointed, Functor, and Monad interface tests
|
||||
- ✅ Parallel vs Sequential execution tests
|
||||
- ✅ Integration tests with complex pipelines
|
||||
- ✅ Error handling scenarios
|
||||
|
||||
**Status:** This package has exemplary test coverage and can serve as a model for other packages.
|
||||
|
||||
### ReaderIOResult Package
|
||||
**Status:** ⚠️ **NO TESTS FOUND**
|
||||
|
||||
**Recommendations:**
|
||||
Create comprehensive test suite covering:
|
||||
- Basic construction and execution
|
||||
- Map, Chain, Ap operations
|
||||
- Error handling
|
||||
- Environment dependency injection
|
||||
- Integration with IOResult
|
||||
|
||||
### ReaderResult Package
|
||||
**Files:** Multiple test files exist
|
||||
- `array_test.go`
|
||||
- `bind_test.go`
|
||||
- `curry_test.go`
|
||||
- `from_test.go`
|
||||
- `monoid_test.go`
|
||||
- `reader_test.go`
|
||||
- `sequence_test.go`
|
||||
|
||||
**Status:** ✅ Good coverage exists
|
||||
|
||||
## Subpackages Review
|
||||
|
||||
### Packages Requiring Review:
|
||||
1. **idiomatic/option/number/** - Needs documentation and test review
|
||||
2. **idiomatic/option/testing/** - Contains disabled test files (`laws_test._go`, `laws._go`)
|
||||
3. **idiomatic/result/exec/** - Needs review
|
||||
4. **idiomatic/result/http/** - Needs review
|
||||
5. **idiomatic/result/testing/** - Contains disabled test files
|
||||
6. **idiomatic/ioresult/exec/** - Needs review
|
||||
7. **idiomatic/ioresult/file/** - Needs review
|
||||
8. **idiomatic/ioresult/http/** - Needs review
|
||||
9. **idiomatic/ioresult/http/builder/** - Needs review
|
||||
10. **idiomatic/ioresult/testing/** - Needs review
|
||||
|
||||
## Priority Recommendations
|
||||
|
||||
### High Priority
|
||||
1. **Enable Commented Tests:** Uncomment and fix tests in `option/option_test.go`
|
||||
2. **Add Missing Option Tests:** Create tests for all untested functions in option package
|
||||
3. **Add Missing Result Tests:** Create comprehensive test suite for result package
|
||||
4. **Create ReaderIOResult Tests:** This package has no tests at all
|
||||
|
||||
### Medium Priority
|
||||
5. **Review Subpackages:** Systematically review exec, file, http, and testing subpackages
|
||||
6. **Enable Testing Package Tests:** Investigate why `laws_test._go` files are disabled
|
||||
|
||||
### Low Priority
|
||||
7. **Benchmark Tests:** Consider adding benchmark tests for performance-critical operations
|
||||
8. **Property-Based Tests:** Consider adding property-based tests using testing/quick
|
||||
|
||||
## Files Modified in This Review
|
||||
|
||||
1. `idiomatic/option/types.go` - Added copyright and documentation
|
||||
2. `idiomatic/option/function.go` - Added copyright and enhanced docs
|
||||
3. `idiomatic/option/option.go` - Enhanced function documentation
|
||||
4. `idiomatic/result/function.go` - Added copyright and enhanced docs
|
||||
5. `idiomatic/readerioresult/doc.go` - **CREATED NEW FILE**
|
||||
6. `idiomatic/readerioresult/types.go` - Added comprehensive type docs
|
||||
7. `idiomatic/readerresult/types.go` - Added comprehensive type docs
|
||||
|
||||
## Summary Statistics
|
||||
|
||||
- **Packages Reviewed:** 6 main packages
|
||||
- **Documentation Files Created:** 1 (readerioresult/doc.go)
|
||||
- **Files Modified:** 7
|
||||
- **Lines of Documentation Added:** ~150+
|
||||
- **Test Coverage Status:**
|
||||
- ✅ Excellent: ioresult
|
||||
- ✅ Good: readerresult
|
||||
- ⚠️ Needs Improvement: option, result
|
||||
- ⚠️ Missing: readerioresult
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Create missing unit tests for option package functions
|
||||
2. Create missing unit tests for result package functions
|
||||
3. Create complete test suite for readerioresult package
|
||||
4. Review and document subpackages (exec, file, http, testing, number)
|
||||
5. Investigate and potentially enable disabled test files in testing subpackages
|
||||
6. Consider adding integration tests that demonstrate real-world usage patterns
|
||||
|
||||
## Conclusion
|
||||
|
||||
The idiomatic package has excellent documentation at the package level, with comprehensive explanations of concepts, usage patterns, and performance characteristics. The main areas for improvement are:
|
||||
|
||||
1. **Test Coverage:** Several functions lack unit tests, particularly in option and result packages
|
||||
2. **Subpackage Documentation:** Some subpackages need documentation review
|
||||
3. **Disabled Tests:** Some test files are disabled and should be investigated
|
||||
|
||||
The IOResult package serves as an excellent example of comprehensive testing, including monad law verification and integration tests. This approach should be replicated across other packages.
|
||||
@@ -27,21 +27,21 @@
|
||||
// Unlike the standard fp-go packages (option, either, result) which use struct wrappers,
|
||||
// the idiomatic package uses Go's native tuple patterns:
|
||||
//
|
||||
// Standard either: Either[E, A] (struct wrapper)
|
||||
// Idiomatic result: (A, error) (native Go tuple)
|
||||
// Standard either: Either[E, A] (struct wrapper)
|
||||
// Idiomatic result: (A, error) (native Go tuple)
|
||||
//
|
||||
// Standard option: Option[A] (struct wrapper)
|
||||
// Idiomatic option: (A, bool) (native Go tuple)
|
||||
// Standard option: Option[A] (struct wrapper)
|
||||
// Idiomatic option: (A, bool) (native Go tuple)
|
||||
//
|
||||
// # Performance Benefits
|
||||
//
|
||||
// The idiomatic approach offers several performance advantages:
|
||||
//
|
||||
// - Zero allocation for creating values (no heap allocations)
|
||||
// - Better CPU cache locality (no pointer indirection)
|
||||
// - Native Go compiler optimizations for tuples
|
||||
// - Reduced garbage collection pressure
|
||||
// - Smaller memory footprint
|
||||
// - Zero allocation for creating values (no heap allocations)
|
||||
// - Better CPU cache locality (no pointer indirection)
|
||||
// - Native Go compiler optimizations for tuples
|
||||
// - Reduced garbage collection pressure
|
||||
// - Smaller memory footprint
|
||||
//
|
||||
// Benchmarks show 2-10x performance improvements for common operations compared to struct-based
|
||||
// implementations, especially for simple operations like Map, Chain, and Fold.
|
||||
@@ -58,7 +58,7 @@
|
||||
//
|
||||
// # Subpackages
|
||||
//
|
||||
// The idiomatic package includes two main subpackages:
|
||||
// The idiomatic package includes three main subpackages:
|
||||
//
|
||||
// ## idiomatic/option
|
||||
//
|
||||
@@ -74,7 +74,7 @@
|
||||
// none := option.None[int]() // (0, false)
|
||||
//
|
||||
// // Transforming values
|
||||
// double := option.Map(func(x int) int { return x * 2 })
|
||||
// double := option.Map(N.Mul(2))
|
||||
// result := double(some) // (84, true)
|
||||
// result = double(none) // (0, false)
|
||||
//
|
||||
@@ -103,7 +103,7 @@
|
||||
// failure := result.Left[int](errors.New("oops")) // (0, error)
|
||||
//
|
||||
// // Transforming values
|
||||
// double := result.Map(func(x int) int { return x * 2 })
|
||||
// double := result.Map(N.Mul(2))
|
||||
// res := double(success) // (84, nil)
|
||||
// res = double(failure) // (0, error)
|
||||
//
|
||||
@@ -126,6 +126,61 @@
|
||||
// // handle error
|
||||
// }
|
||||
//
|
||||
// ## idiomatic/ioresult
|
||||
//
|
||||
// Implements the IOResult monad using func() (value, error) for IO operations that can fail.
|
||||
// This combines IO effects (side-effectful operations) with Go's standard error handling pattern.
|
||||
// It's the idiomatic version of IOEither, representing computations that perform side effects
|
||||
// and may fail.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// import "github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
//
|
||||
// // Creating IOResult values
|
||||
// success := ioresult.Of(42) // func() (int, error) returning (42, nil)
|
||||
// failure := ioresult.Left[int](errors.New("oops")) // func() (int, error) returning (0, error)
|
||||
//
|
||||
// // Reading a file with IOResult
|
||||
// readConfig := ioresult.FromIO(func() string {
|
||||
// return "config.json"
|
||||
// })
|
||||
//
|
||||
// // Transforming IO operations
|
||||
// processFile := F.Pipe2(
|
||||
// readConfig,
|
||||
// ioresult.Map(strings.ToUpper),
|
||||
// ioresult.Chain(func(path string) ioresult.IOResult[[]byte] {
|
||||
// return func() ([]byte, error) {
|
||||
// return os.ReadFile(path)
|
||||
// }
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
// // Execute the IO operation
|
||||
// content, err := processFile()
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// // Resource management with Bracket
|
||||
// result, err := ioresult.Bracket(
|
||||
// func() (*os.File, error) { return os.Open("data.txt") },
|
||||
// func(f *os.File, err error) ioresult.IOResult[any] {
|
||||
// return func() (any, error) { return nil, f.Close() }
|
||||
// },
|
||||
// func(f *os.File) ioresult.IOResult[[]byte] {
|
||||
// return func() ([]byte, error) { return io.ReadAll(f) }
|
||||
// },
|
||||
// )()
|
||||
//
|
||||
// Key features:
|
||||
// - Lazy evaluation: Operations are not executed until the IOResult is called
|
||||
// - Composable: Chain IO operations that may fail
|
||||
// - Error handling: Automatic error propagation and recovery
|
||||
// - Resource safety: Bracket ensures proper resource cleanup
|
||||
// - Parallel execution: ApPar and TraverseArrayPar for concurrent operations
|
||||
//
|
||||
// # Type Signatures
|
||||
//
|
||||
// The idiomatic packages use function types that work naturally with Go tuples:
|
||||
@@ -140,21 +195,42 @@
|
||||
// Operator[A, B any] = func(A, error) (B, error) // Transform Result[A] to Result[B]
|
||||
// Kleisli[A, B any] = func(A) (B, error) // Monadic function from A to Result[B]
|
||||
//
|
||||
// For ioresult package:
|
||||
//
|
||||
// IOResult[A any] = func() (A, error) // IO operation returning A or error
|
||||
// Operator[A, B any] = func(IOResult[A]) IOResult[B] // Transform IOResult[A] to IOResult[B]
|
||||
// Kleisli[A, B any] = func(A) IOResult[B] // Monadic function from A to IOResult[B]
|
||||
//
|
||||
// # When to Use Idiomatic vs Standard Packages
|
||||
//
|
||||
// Use idiomatic packages when:
|
||||
// - Performance is critical (hot paths, tight loops)
|
||||
// - You want zero-allocation functional patterns
|
||||
// - You prefer Go's native error handling style
|
||||
// - You're integrating with existing Go code that uses tuples
|
||||
// - Memory efficiency matters (embedded systems, high-scale services)
|
||||
// - Performance is critical (hot paths, tight loops)
|
||||
// - You want zero-allocation functional patterns
|
||||
// - You prefer Go's native error handling style
|
||||
// - You're integrating with existing Go code that uses tuples
|
||||
// - Memory efficiency matters (embedded systems, high-scale services)
|
||||
// - You need IO operations with error handling (use ioresult)
|
||||
//
|
||||
// Use standard packages when:
|
||||
// - You need full algebraic data type semantics
|
||||
// - You're porting code from other FP languages
|
||||
// - You want explicit Either[E, A] with custom error types
|
||||
// - You need the complete suite of FP abstractions
|
||||
// - Code clarity outweighs performance concerns
|
||||
// - You need full algebraic data type semantics
|
||||
// - You're porting code from other FP languages
|
||||
// - You want explicit Either[E, A] with custom error types
|
||||
// - You need the complete suite of FP abstractions
|
||||
// - Code clarity outweighs performance concerns
|
||||
//
|
||||
// # Choosing Between result and ioresult
|
||||
//
|
||||
// Use result when:
|
||||
// - Operations are pure (same input always produces same output)
|
||||
// - No side effects are involved (no IO, no state mutation)
|
||||
// - You want to represent success/failure without execution delay
|
||||
//
|
||||
// Use ioresult when:
|
||||
// - Operations perform IO (file system, network, database)
|
||||
// - Side effects are part of the computation
|
||||
// - You need lazy evaluation (defer execution until needed)
|
||||
// - You want to compose IO operations that may fail
|
||||
// - Resource management is required (files, connections, locks)
|
||||
//
|
||||
// # Performance Comparison
|
||||
//
|
||||
@@ -226,6 +302,43 @@
|
||||
// result.Map(format),
|
||||
// )
|
||||
//
|
||||
// ## IO Pipeline with IOResult
|
||||
//
|
||||
// Compose IO operations that may fail:
|
||||
//
|
||||
// import (
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// "github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
// )
|
||||
//
|
||||
// // Define IO operations
|
||||
// readFile := func(path string) ioresult.IOResult[[]byte] {
|
||||
// return func() ([]byte, error) {
|
||||
// return os.ReadFile(path)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// parseJSON := func(data []byte) ioresult.IOResult[Config] {
|
||||
// return func() (Config, error) {
|
||||
// var cfg Config
|
||||
// err := json.Unmarshal(data, &cfg)
|
||||
// return cfg, err
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Compose operations (not executed yet)
|
||||
// loadConfig := F.Pipe1(
|
||||
// readFile("config.json"),
|
||||
// ioresult.Chain(parseJSON),
|
||||
// ioresult.Map(validateConfig),
|
||||
// )
|
||||
//
|
||||
// // Execute the IO pipeline
|
||||
// config, err := loadConfig()
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// ## Error Accumulation with Validation
|
||||
//
|
||||
// The idiomatic/result package supports validation patterns for accumulating multiple errors:
|
||||
@@ -284,6 +397,41 @@
|
||||
// return user, true
|
||||
// }
|
||||
//
|
||||
// // File operations with IOResult and resource safety
|
||||
// import "github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
//
|
||||
// processFile := ioresult.Bracket(
|
||||
// // Acquire resource
|
||||
// func() (*os.File, error) {
|
||||
// return os.Open("data.txt")
|
||||
// },
|
||||
// // Release resource (always called)
|
||||
// func(f *os.File, err error) ioresult.IOResult[any] {
|
||||
// return func() (any, error) {
|
||||
// return nil, f.Close()
|
||||
// }
|
||||
// },
|
||||
// // Use resource
|
||||
// func(f *os.File) ioresult.IOResult[string] {
|
||||
// return func() (string, error) {
|
||||
// data, err := io.ReadAll(f)
|
||||
// return string(data), err
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
// content, err := processFile()
|
||||
//
|
||||
// // System command execution with IOResult
|
||||
// import ioexec "github.com/IBM/fp-go/v2/idiomatic/ioresult/exec"
|
||||
//
|
||||
// version := F.Pipe1(
|
||||
// ioexec.Command("git")([]string{"version"})([]byte{}),
|
||||
// ioresult.Map(func(output exec.CommandOutput) string {
|
||||
// return string(exec.StdOut(output))
|
||||
// }),
|
||||
// )
|
||||
// result, err := version()
|
||||
//
|
||||
// # Best Practices
|
||||
//
|
||||
// 1. Use descriptive error messages:
|
||||
@@ -325,7 +473,7 @@
|
||||
// result, err := F.Pipe2(
|
||||
// input,
|
||||
// result.Right[int],
|
||||
// result.Map(func(x int) int { return x * 2 }),
|
||||
// result.Map(N.Mul(2)),
|
||||
// )
|
||||
// assert.NoError(t, err)
|
||||
// assert.Equal(t, 42, result)
|
||||
@@ -335,7 +483,7 @@
|
||||
// value, ok := F.Pipe2(
|
||||
// 42,
|
||||
// option.Some[int],
|
||||
// option.Map(func(x int) int { return x * 2 }),
|
||||
// option.Map(N.Mul(2)),
|
||||
// )
|
||||
// assert.True(t, ok)
|
||||
// assert.Equal(t, 84, value)
|
||||
@@ -348,8 +496,10 @@
|
||||
// - Standard option package: github.com/IBM/fp-go/v2/option
|
||||
// - Standard either package: github.com/IBM/fp-go/v2/either
|
||||
// - Standard result package: github.com/IBM/fp-go/v2/result
|
||||
// - Standard ioeither package: github.com/IBM/fp-go/v2/ioeither
|
||||
//
|
||||
// See the subpackage documentation for detailed API references:
|
||||
// - idiomatic/option: Option monad using (value, bool) tuples
|
||||
// - idiomatic/result: Result/Either monad using (value, error) tuples
|
||||
// - idiomatic/ioresult: IOResult monad using func() (value, error) for IO operations
|
||||
package idiomatic
|
||||
|
||||
196
v2/idiomatic/ioresult/BENCHMARKS.md
Normal file
196
v2/idiomatic/ioresult/BENCHMARKS.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# IOResult Benchmark Results
|
||||
|
||||
Performance benchmarks for the `idiomatic/ioresult` package.
|
||||
|
||||
**Test Environment:**
|
||||
- CPU: AMD Ryzen 7 PRO 7840U w/ Radeon 780M Graphics
|
||||
- OS: Windows
|
||||
- Architecture: amd64
|
||||
- Go version: go1.23+
|
||||
|
||||
## Summary
|
||||
|
||||
The `idiomatic/ioresult` package demonstrates exceptional performance with **zero allocations** for most core operations. The package achieves sub-nanosecond performance for basic operations like `Of`, `Map`, and `Chain`.
|
||||
|
||||
## Core Operations
|
||||
|
||||
### Basic Construction
|
||||
|
||||
| Operation | ns/op | B/op | allocs/op |
|
||||
|-----------|-------|------|-----------|
|
||||
| **Of** (Success) | 0.22 | 0 | 0 |
|
||||
| **Left** (Error) | 0.22 | 0 | 0 |
|
||||
| **FromIO** | 0.48 | 0 | 0 |
|
||||
|
||||
**Analysis:** Creating IOResult values has effectively zero overhead with no allocations.
|
||||
|
||||
### Functor Operations
|
||||
|
||||
| Operation | ns/op | B/op | allocs/op |
|
||||
|-----------|-------|------|-----------|
|
||||
| **Map** (Success) | 0.22 | 0 | 0 |
|
||||
| **Map** (Error) | 0.22 | 0 | 0 |
|
||||
| **Functor.Map** | 133.30 | 80 | 3 |
|
||||
|
||||
**Analysis:** Direct `Map` operation has zero overhead. Using the `Functor` interface adds some allocation overhead due to interface wrapping.
|
||||
|
||||
### Monad Operations
|
||||
|
||||
| Operation | ns/op | B/op | allocs/op |
|
||||
|-----------|-------|------|-----------|
|
||||
| **Chain** (Success) | 0.21 | 0 | 0 |
|
||||
| **Chain** (Error) | 0.22 | 0 | 0 |
|
||||
| **Monad.Chain** | 317.70 | 104 | 4 |
|
||||
| **Pointed.Of** | 35.32 | 24 | 1 |
|
||||
|
||||
**Analysis:** Direct monad operations are extremely fast. Using the `Monad` interface adds overhead but is still performant for real-world use.
|
||||
|
||||
### Applicative Operations
|
||||
|
||||
| Operation | ns/op | B/op | allocs/op |
|
||||
|-----------|-------|------|-----------|
|
||||
| **ApFirst** | 41.02 | 48 | 2 |
|
||||
| **ApSecond** | 92.43 | 104 | 4 |
|
||||
| **MonadApFirst** | 96.61 | 80 | 3 |
|
||||
| **MonadApSecond** | 216.50 | 104 | 4 |
|
||||
|
||||
**Analysis:** Applicative operations involve parallel execution and have modest allocation overhead.
|
||||
|
||||
### Error Handling
|
||||
|
||||
| Operation | ns/op | B/op | allocs/op |
|
||||
|-----------|-------|------|-----------|
|
||||
| **Alt** | 0.55 | 0 | 0 |
|
||||
| **GetOrElse** | 0.62 | 0 | 0 |
|
||||
| **Fold** | 168.20 | 128 | 4 |
|
||||
|
||||
**Analysis:** Error recovery operations like `Alt` and `GetOrElse` are extremely efficient. `Fold` has overhead due to wrapping both branches in IO.
|
||||
|
||||
### Chain Operations
|
||||
|
||||
| Operation | ns/op | B/op | allocs/op |
|
||||
|-----------|-------|------|-----------|
|
||||
| **ChainIOK** | 215.30 | 128 | 5 |
|
||||
| **ChainFirst** | 239.90 | 128 | 5 |
|
||||
|
||||
**Analysis:** Specialized chain operations have predictable allocation patterns.
|
||||
|
||||
## Pipeline Performance
|
||||
|
||||
### Simple Pipelines
|
||||
|
||||
| Operation | ns/op | B/op | allocs/op |
|
||||
|-----------|-------|------|-----------|
|
||||
| **Pipeline** (3 Maps) | 0.87 | 0 | 0 |
|
||||
| **ChainSequence** (3 Chains) | 0.95 | 0 | 0 |
|
||||
|
||||
**Analysis:** Composing operations through pipes has zero allocation overhead. The cost is purely computational.
|
||||
|
||||
### Execution Performance
|
||||
|
||||
| Operation | ns/op | B/op | allocs/op |
|
||||
|-----------|-------|------|-----------|
|
||||
| **Execute** (Simple) | 0.22 | 0 | 0 |
|
||||
| **ExecutePipeline** (3 Maps) | 5.67 | 0 | 0 |
|
||||
|
||||
**Analysis:** Executing IOResult operations is very fast. Even complex pipelines execute in nanoseconds.
|
||||
|
||||
## Collection Operations
|
||||
|
||||
| Operation | ns/op | B/op | allocs/op | Items |
|
||||
|-----------|-------|------|-----------|-------|
|
||||
| **TraverseArray** | 1,883 | 1,592 | 59 | 10 |
|
||||
| **SequenceArray** | 1,051 | 808 | 30 | 5 |
|
||||
|
||||
**Analysis:** Collection operations have O(n) allocation behavior. Performance scales linearly with input size.
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Bind Operations
|
||||
|
||||
| Operation | ns/op | B/op | allocs/op |
|
||||
|-----------|-------|------|-----------|
|
||||
| **Bind** | 167.40 | 184 | 7 |
|
||||
| **Bind** (with allocation tracking) | 616.10 | 336 | 13 |
|
||||
| **DirectChainMap** | 82.42 | 48 | 2 |
|
||||
|
||||
**Analysis:** `Bind` provides do-notation convenience at a modest performance cost. Direct chaining is more efficient when performance is critical.
|
||||
|
||||
### Pattern Performance
|
||||
|
||||
#### Map Patterns
|
||||
| Pattern | ns/op | B/op | allocs/op |
|
||||
|---------|-------|------|-----------|
|
||||
| SimpleFunction | 0.55 | 0 | 0 |
|
||||
| InlinedLambda | 0.69 | 0 | 0 |
|
||||
| NestedMaps (3x) | 10.54 | 0 | 0 |
|
||||
|
||||
#### Of Patterns
|
||||
| Pattern | ns/op | B/op | allocs/op |
|
||||
|---------|-------|------|-----------|
|
||||
| IntValue | 0.44 | 0 | 0 |
|
||||
| StructValue | 0.43 | 0 | 0 |
|
||||
| PointerValue | 0.46 | 0 | 0 |
|
||||
|
||||
#### Chain Patterns
|
||||
| Pattern | ns/op | B/op | allocs/op |
|
||||
|---------|-------|------|-----------|
|
||||
| SimpleChain | 0.46 | 0 | 0 |
|
||||
| ChainSequence (5x) | 47.75 | 24 | 1 |
|
||||
|
||||
### Error Path Performance
|
||||
|
||||
| Path | ns/op | B/op | allocs/op |
|
||||
|------|-------|------|-----------|
|
||||
| **SuccessPath** | 0.91 | 0 | 0 |
|
||||
| **ErrorPath** | 1.44 | 0 | 0 |
|
||||
|
||||
**Analysis:** Error paths are slightly slower than success paths but still sub-nanosecond. Both paths have zero allocations.
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
### Zero-Allocation Operations
|
||||
The following operations have **zero heap allocations**:
|
||||
- `Of`, `Left` (construction)
|
||||
- `Map`, `Chain` (transformation)
|
||||
- `Alt`, `GetOrElse` (error recovery)
|
||||
- `FromIO` (conversion)
|
||||
- Pipeline composition
|
||||
- Execution of simple operations
|
||||
|
||||
### Low-Allocation Operations
|
||||
The following operations have minimal allocations:
|
||||
- Interface-based operations (Functor, Monad, Pointed): 1-4 allocations
|
||||
- Applicative operations (ApFirst, ApSecond): 2-4 allocations
|
||||
- Collection operations: O(n) allocations based on input size
|
||||
|
||||
### Optimization Opportunities
|
||||
|
||||
1. **Prefer direct functions over interfaces**: Using `Map` directly is ~600x faster than `Functor.Map` due to interface overhead
|
||||
2. **Avoid unnecessary Bind**: Direct chaining with `Chain` and `Map` is 7-10x faster than `Bind`
|
||||
3. **Use parallel operations judiciously**: ApFirst/ApSecond have overhead; only use when parallelism is beneficial
|
||||
4. **Batch collection operations**: TraverseArray is efficient for bulk operations rather than multiple individual operations
|
||||
|
||||
## Comparison with Standard IOEither
|
||||
|
||||
The idiomatic implementation aims for:
|
||||
- **Zero allocations** for basic operations (vs 1-2 allocations in standard)
|
||||
- **Sub-nanosecond** performance for core operations
|
||||
- **Native Go idioms** using (value, error) tuples
|
||||
|
||||
## Conclusions
|
||||
|
||||
The `idiomatic/ioresult` package delivers exceptional performance:
|
||||
|
||||
1. **Ultra-fast core operations**: Most operations complete in sub-nanosecond time
|
||||
2. **Zero-allocation design**: Core operations don't allocate memory
|
||||
3. **Predictable performance**: Overhead is consistent and measurable
|
||||
4. **Scalable**: Collection operations scale linearly
|
||||
5. **Production-ready**: Performance characteristics suitable for high-throughput systems
|
||||
|
||||
The package successfully provides functional programming abstractions with minimal runtime overhead, making it suitable for performance-critical applications while maintaining composability and type safety.
|
||||
|
||||
---
|
||||
|
||||
*Benchmarks run on: 2025-11-19*
|
||||
*Command: `go test -bench=. -benchmem -benchtime=1s`*
|
||||
70
v2/idiomatic/ioresult/ap.go
Normal file
70
v2/idiomatic/ioresult/ap.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// 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 ioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/internal/apply"
|
||||
)
|
||||
|
||||
// MonadApFirst combines two effectful actions, keeping only the result of the first.
|
||||
//
|
||||
//go:inline
|
||||
func MonadApFirst[A, B any](first IOResult[A], second IOResult[B]) IOResult[A] {
|
||||
return apply.MonadApFirst(
|
||||
MonadAp[A, B],
|
||||
MonadMap[A, func(B) A],
|
||||
|
||||
first,
|
||||
second,
|
||||
)
|
||||
}
|
||||
|
||||
// ApFirst combines two effectful actions, keeping only the result of the first.
|
||||
//
|
||||
//go:inline
|
||||
func ApFirst[A, B any](second IOResult[B]) Operator[A, A] {
|
||||
return apply.ApFirst(
|
||||
Ap[A, B],
|
||||
Map[A, func(B) A],
|
||||
|
||||
second,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadApSecond combines two effectful actions, keeping only the result of the second.
|
||||
//
|
||||
//go:inline
|
||||
func MonadApSecond[A, B any](first IOResult[A], second IOResult[B]) IOResult[B] {
|
||||
return apply.MonadApSecond(
|
||||
MonadAp[B, B],
|
||||
MonadMap[A, func(B) B],
|
||||
|
||||
first,
|
||||
second,
|
||||
)
|
||||
}
|
||||
|
||||
// ApSecond combines two effectful actions, keeping only the result of the second.
|
||||
//
|
||||
//go:inline
|
||||
func ApSecond[A, B any](second IOResult[B]) Operator[A, B] {
|
||||
return apply.ApSecond(
|
||||
Ap[B, B],
|
||||
Map[A, func(B) B],
|
||||
|
||||
second,
|
||||
)
|
||||
}
|
||||
431
v2/idiomatic/ioresult/ap_test.go
Normal file
431
v2/idiomatic/ioresult/ap_test.go
Normal file
@@ -0,0 +1,431 @@
|
||||
// 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 ioresult
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMonadApFirst(t *testing.T) {
|
||||
t.Run("Both Right - keeps first value", func(t *testing.T) {
|
||||
first := Of("first")
|
||||
second := Of("second")
|
||||
result := MonadApFirst(first, second)
|
||||
|
||||
val, err := result()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "first", val)
|
||||
})
|
||||
|
||||
t.Run("First Right, Second Left - returns second's error", func(t *testing.T) {
|
||||
first := Of(42)
|
||||
second := Left[string](errors.New("second error"))
|
||||
result := MonadApFirst(first, second)
|
||||
|
||||
_, err := result()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "second error", err.Error())
|
||||
})
|
||||
|
||||
t.Run("First Left, Second Right - returns first's error", func(t *testing.T) {
|
||||
first := Left[int](errors.New("first error"))
|
||||
second := Of("success")
|
||||
result := MonadApFirst(first, second)
|
||||
|
||||
_, err := result()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "first error", err.Error())
|
||||
})
|
||||
|
||||
t.Run("Both Left - returns first error", func(t *testing.T) {
|
||||
first := Left[int](errors.New("first error"))
|
||||
second := Left[string](errors.New("second error"))
|
||||
result := MonadApFirst(first, second)
|
||||
|
||||
_, err := result()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "first error", err.Error())
|
||||
})
|
||||
|
||||
t.Run("Different types", func(t *testing.T) {
|
||||
first := Of(123)
|
||||
second := Of("text")
|
||||
result := MonadApFirst(first, second)
|
||||
|
||||
val, err := result()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 123, val)
|
||||
})
|
||||
|
||||
t.Run("Side effects execute for both", func(t *testing.T) {
|
||||
firstCalled := false
|
||||
secondCalled := false
|
||||
|
||||
first := func() (int, error) {
|
||||
firstCalled = true
|
||||
return 1, nil
|
||||
}
|
||||
second := func() (string, error) {
|
||||
secondCalled = true
|
||||
return "test", nil
|
||||
}
|
||||
|
||||
result := MonadApFirst(first, second)
|
||||
val, err := result()
|
||||
|
||||
assert.True(t, firstCalled)
|
||||
assert.True(t, secondCalled)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, val)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApFirst(t *testing.T) {
|
||||
t.Run("Both Right - keeps first value", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of("first"),
|
||||
ApFirst[string](Of("second")),
|
||||
)
|
||||
|
||||
val, err := result()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "first", val)
|
||||
})
|
||||
|
||||
t.Run("First Right, Second Left - returns error", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of(100),
|
||||
ApFirst[int](Left[string](errors.New("error"))),
|
||||
)
|
||||
|
||||
_, err := result()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "error", err.Error())
|
||||
})
|
||||
|
||||
t.Run("First Left, Second Right - returns error", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Left[int](errors.New("first error")),
|
||||
ApFirst[int](Of("success")),
|
||||
)
|
||||
|
||||
_, err := result()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "first error", err.Error())
|
||||
})
|
||||
|
||||
t.Run("Chain multiple ApFirst", func(t *testing.T) {
|
||||
result := F.Pipe2(
|
||||
Of("value"),
|
||||
ApFirst[string](Of(1)),
|
||||
ApFirst[string](Of(true)),
|
||||
)
|
||||
|
||||
val, err := result()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "value", val)
|
||||
})
|
||||
|
||||
t.Run("With Map composition", func(t *testing.T) {
|
||||
result := F.Pipe2(
|
||||
Of(5),
|
||||
ApFirst[int](Of("ignored")),
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
|
||||
val, err := result()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 10, val)
|
||||
})
|
||||
|
||||
t.Run("Side effect in second executes", func(t *testing.T) {
|
||||
effectExecuted := false
|
||||
second := func() (string, error) {
|
||||
effectExecuted = true
|
||||
return "effect", nil
|
||||
}
|
||||
|
||||
result := F.Pipe1(
|
||||
Of(42),
|
||||
ApFirst[int](second),
|
||||
)
|
||||
|
||||
val, err := result()
|
||||
assert.True(t, effectExecuted)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, val)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadApSecond(t *testing.T) {
|
||||
t.Run("Both Right - keeps second value", func(t *testing.T) {
|
||||
first := Of("first")
|
||||
second := Of("second")
|
||||
result := MonadApSecond(first, second)
|
||||
|
||||
val, err := result()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "second", val)
|
||||
})
|
||||
|
||||
t.Run("First Right, Second Left - returns second's error", func(t *testing.T) {
|
||||
first := Of(42)
|
||||
second := Left[string](errors.New("second error"))
|
||||
result := MonadApSecond(first, second)
|
||||
|
||||
_, err := result()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "second error", err.Error())
|
||||
})
|
||||
|
||||
t.Run("First Left, Second Right - returns first's error", func(t *testing.T) {
|
||||
first := Left[int](errors.New("first error"))
|
||||
second := Of("success")
|
||||
result := MonadApSecond(first, second)
|
||||
|
||||
_, err := result()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "first error", err.Error())
|
||||
})
|
||||
|
||||
t.Run("Both Left - returns first error", func(t *testing.T) {
|
||||
first := Left[int](errors.New("first error"))
|
||||
second := Left[string](errors.New("second error"))
|
||||
result := MonadApSecond(first, second)
|
||||
|
||||
_, err := result()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "first error", err.Error())
|
||||
})
|
||||
|
||||
t.Run("Different types", func(t *testing.T) {
|
||||
first := Of(123)
|
||||
second := Of("text")
|
||||
result := MonadApSecond(first, second)
|
||||
|
||||
val, err := result()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "text", val)
|
||||
})
|
||||
|
||||
t.Run("Side effects execute for both", func(t *testing.T) {
|
||||
firstCalled := false
|
||||
secondCalled := false
|
||||
|
||||
first := func() (int, error) {
|
||||
firstCalled = true
|
||||
return 1, nil
|
||||
}
|
||||
second := func() (string, error) {
|
||||
secondCalled = true
|
||||
return "test", nil
|
||||
}
|
||||
|
||||
result := MonadApSecond(first, second)
|
||||
val, err := result()
|
||||
|
||||
assert.True(t, firstCalled)
|
||||
assert.True(t, secondCalled)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test", val)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApSecond(t *testing.T) {
|
||||
t.Run("Both Right - keeps second value", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of("first"),
|
||||
ApSecond[string](Of("second")),
|
||||
)
|
||||
|
||||
val, err := result()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "second", val)
|
||||
})
|
||||
|
||||
t.Run("First Right, Second Left - returns error", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of(100),
|
||||
ApSecond[int](Left[string](errors.New("error"))),
|
||||
)
|
||||
|
||||
_, err := result()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "error", err.Error())
|
||||
})
|
||||
|
||||
t.Run("First Left, Second Right - returns error", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Left[int](errors.New("first error")),
|
||||
ApSecond[int](Of("success")),
|
||||
)
|
||||
|
||||
_, err := result()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "first error", err.Error())
|
||||
})
|
||||
|
||||
t.Run("Chain multiple ApSecond", func(t *testing.T) {
|
||||
result := F.Pipe2(
|
||||
Of("initial"),
|
||||
ApSecond[string](Of("middle")),
|
||||
ApSecond[string](Of("final")),
|
||||
)
|
||||
|
||||
val, err := result()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "final", val)
|
||||
})
|
||||
|
||||
t.Run("With Map composition", func(t *testing.T) {
|
||||
result := F.Pipe2(
|
||||
Of(1),
|
||||
ApSecond[int](Of(5)),
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
|
||||
val, err := result()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 10, val)
|
||||
})
|
||||
|
||||
t.Run("Side effect in first executes", func(t *testing.T) {
|
||||
effectExecuted := false
|
||||
first := func() (int, error) {
|
||||
effectExecuted = true
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
result := F.Pipe1(
|
||||
first,
|
||||
ApSecond[int](Of("result")),
|
||||
)
|
||||
|
||||
val, err := result()
|
||||
assert.True(t, effectExecuted)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "result", val)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApFirstApSecondInteraction(t *testing.T) {
|
||||
t.Run("ApFirst then ApSecond", func(t *testing.T) {
|
||||
// ApFirst keeps "first", then ApSecond discards it for "second"
|
||||
result := F.Pipe2(
|
||||
Of("first"),
|
||||
ApFirst[string](Of("middle")),
|
||||
ApSecond[string](Of("second")),
|
||||
)
|
||||
|
||||
val, err := result()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "second", val)
|
||||
})
|
||||
|
||||
t.Run("ApSecond then ApFirst", func(t *testing.T) {
|
||||
// ApSecond picks "middle", then ApFirst keeps that over "ignored"
|
||||
result := F.Pipe2(
|
||||
Of("first"),
|
||||
ApSecond[string](Of("middle")),
|
||||
ApFirst[string](Of("ignored")),
|
||||
)
|
||||
|
||||
val, err := result()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "middle", val)
|
||||
})
|
||||
|
||||
t.Run("Error propagation in chain", func(t *testing.T) {
|
||||
result := F.Pipe2(
|
||||
Of("first"),
|
||||
ApFirst[string](Left[int](errors.New("error in middle"))),
|
||||
ApSecond[string](Of("second")),
|
||||
)
|
||||
|
||||
_, err := result()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "error in middle", err.Error())
|
||||
})
|
||||
}
|
||||
|
||||
func TestApSequencingBehavior(t *testing.T) {
|
||||
t.Run("MonadApFirst executes both operations", func(t *testing.T) {
|
||||
executionOrder := make([]string, 2)
|
||||
|
||||
first := func() (int, error) {
|
||||
executionOrder[0] = "first"
|
||||
return 1, nil
|
||||
}
|
||||
second := func() (string, error) {
|
||||
executionOrder[1] = "second"
|
||||
return "test", nil
|
||||
}
|
||||
|
||||
result := MonadApFirst(first, second)
|
||||
_, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
// Note: execution order is second then first due to applicative implementation
|
||||
assert.Len(t, executionOrder, 2)
|
||||
assert.Contains(t, executionOrder, "first")
|
||||
assert.Contains(t, executionOrder, "second")
|
||||
})
|
||||
|
||||
t.Run("MonadApSecond executes both operations", func(t *testing.T) {
|
||||
executionOrder := make([]string, 2)
|
||||
|
||||
first := func() (int, error) {
|
||||
executionOrder[0] = "first"
|
||||
return 1, nil
|
||||
}
|
||||
second := func() (string, error) {
|
||||
executionOrder[1] = "second"
|
||||
return "test", nil
|
||||
}
|
||||
|
||||
result := MonadApSecond(first, second)
|
||||
_, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
// Note: execution order is second then first due to applicative implementation
|
||||
assert.Len(t, executionOrder, 2)
|
||||
assert.Contains(t, executionOrder, "first")
|
||||
assert.Contains(t, executionOrder, "second")
|
||||
})
|
||||
|
||||
t.Run("Error in first stops second from affecting result in MonadApFirst", func(t *testing.T) {
|
||||
secondExecuted := false
|
||||
|
||||
first := Left[int](errors.New("first error"))
|
||||
second := func() (string, error) {
|
||||
secondExecuted = true
|
||||
return "test", nil
|
||||
}
|
||||
|
||||
result := MonadApFirst(first, second)
|
||||
_, err := result()
|
||||
|
||||
// Second still executes but error is from first
|
||||
assert.True(t, secondExecuted)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "first error", err.Error())
|
||||
})
|
||||
}
|
||||
50
v2/idiomatic/ioresult/bench_results.txt
Normal file
50
v2/idiomatic/ioresult/bench_results.txt
Normal file
@@ -0,0 +1,50 @@
|
||||
2025/11/19 15:31:45 Data:
|
||||
{
|
||||
"key": "key",
|
||||
"value": "value"
|
||||
}
|
||||
2025/11/19 15:31:45 t1: foo, t2: 1, t3: foo1
|
||||
goos: windows
|
||||
goarch: amd64
|
||||
pkg: github.com/IBM/fp-go/v2/idiomatic/ioresult
|
||||
cpu: AMD Ryzen 7 PRO 7840U w/ Radeon 780M Graphics
|
||||
BenchmarkOf-16 1000000000 0.2241 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkMap-16 1000000000 0.2151 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkChain-16 1000000000 0.2100 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkBind-16 7764834 167.4 ns/op 184 B/op 7 allocs/op
|
||||
BenchmarkPipeline-16 1000000000 0.8716 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkExecute-16 1000000000 0.2231 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkExecutePipeline-16 231766047 5.671 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkChainSequence-16 1000000000 0.9532 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkLeft-16 1000000000 0.2233 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkMapWithError-16 1000000000 0.2172 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkChainWithError-16 1000000000 0.2151 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkApFirst-16 31478382 41.02 ns/op 48 B/op 2 allocs/op
|
||||
BenchmarkApSecond-16 13940001 92.43 ns/op 104 B/op 4 allocs/op
|
||||
BenchmarkMonadApFirst-16 18065812 96.61 ns/op 80 B/op 3 allocs/op
|
||||
BenchmarkMonadApSecond-16 8618354 216.5 ns/op 104 B/op 4 allocs/op
|
||||
BenchmarkFunctor-16 9963952 133.3 ns/op 80 B/op 3 allocs/op
|
||||
BenchmarkMonad-16 7325534 317.7 ns/op 104 B/op 4 allocs/op
|
||||
BenchmarkPointed-16 30711267 35.32 ns/op 24 B/op 1 allocs/op
|
||||
BenchmarkTraverseArray-16 652627 1883 ns/op 1592 B/op 59 allocs/op
|
||||
BenchmarkSequenceArray-16 1000000 1051 ns/op 808 B/op 30 allocs/op
|
||||
BenchmarkAlt-16 1000000000 0.5538 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGetOrElse-16 1000000000 0.6245 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkFold-16 7670691 168.2 ns/op 128 B/op 4 allocs/op
|
||||
BenchmarkFromIO-16 1000000000 0.4832 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkChainIOK-16 5704785 215.3 ns/op 128 B/op 5 allocs/op
|
||||
BenchmarkChainFirst-16 4841002 239.9 ns/op 128 B/op 5 allocs/op
|
||||
BenchmarkBindAllocations/Bind-16 1988592 616.1 ns/op 336 B/op 13 allocs/op
|
||||
BenchmarkBindAllocations/DirectChainMap-16 14671629 82.42 ns/op 48 B/op 2 allocs/op
|
||||
BenchmarkMapPatterns/SimpleFunction-16 1000000000 0.5509 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkMapPatterns/InlinedLambda-16 1000000000 0.6880 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkMapPatterns/NestedMaps-16 100000000 10.54 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkOfPatterns/IntValue-16 1000000000 0.4405 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkOfPatterns/StructValue-16 1000000000 0.4252 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkOfPatterns/PointerValue-16 1000000000 0.4587 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkChainPatterns/SimpleChain-16 1000000000 0.4593 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkChainPatterns/ChainSequence-16 25868368 47.75 ns/op 24 B/op 1 allocs/op
|
||||
BenchmarkErrorPaths/SuccessPath-16 1000000000 0.9135 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkErrorPaths/ErrorPath-16 787269846 1.438 ns/op 0 B/op 0 allocs/op
|
||||
PASS
|
||||
ok github.com/IBM/fp-go/v2/idiomatic/ioresult 44.835s
|
||||
302
v2/idiomatic/ioresult/bench_test.go
Normal file
302
v2/idiomatic/ioresult/bench_test.go
Normal file
@@ -0,0 +1,302 @@
|
||||
// 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 ioresult
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
func BenchmarkOf(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = Of(42)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMap(b *testing.B) {
|
||||
io := Of(42)
|
||||
f := N.Mul(2)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = Map(f)(io)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChain(b *testing.B) {
|
||||
io := Of(42)
|
||||
f := func(x int) IOResult[int] { return Of(x * 2) }
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = Chain(f)(io)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBind(b *testing.B) {
|
||||
type Data struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
io := Of(Data{Value: 0})
|
||||
f := func(d Data) IOResult[int] { return Of(d.Value * 2) }
|
||||
setter := func(v int) func(Data) Data {
|
||||
return func(d Data) Data {
|
||||
d.Value = v
|
||||
return d
|
||||
}
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = Bind(setter, f)(io)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPipeline(b *testing.B) {
|
||||
f1 := N.Add(1)
|
||||
f2 := N.Mul(2)
|
||||
f3 := N.Sub(3)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = F.Pipe3(
|
||||
Of(42),
|
||||
Map(f1),
|
||||
Map(f2),
|
||||
Map(f3),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExecute(b *testing.B) {
|
||||
io := Of(42)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = io()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExecutePipeline(b *testing.B) {
|
||||
f1 := N.Add(1)
|
||||
f2 := N.Mul(2)
|
||||
f3 := N.Sub(3)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
io := F.Pipe3(
|
||||
Of(42),
|
||||
Map(f1),
|
||||
Map(f2),
|
||||
Map(f3),
|
||||
)
|
||||
_, _ = io()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChainSequence(b *testing.B) {
|
||||
f1 := func(x int) IOResult[int] { return Of(x + 1) }
|
||||
f2 := func(x int) IOResult[int] { return Of(x * 2) }
|
||||
f3 := func(x int) IOResult[int] { return Of(x - 3) }
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = F.Pipe3(
|
||||
Of(42),
|
||||
Chain(f1),
|
||||
Chain(f2),
|
||||
Chain(f3),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLeft(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = Left[int](F.Constant[error](nil)())
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMapWithError(b *testing.B) {
|
||||
io := Left[int](F.Constant[error](nil)())
|
||||
f := N.Mul(2)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = Map(f)(io)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChainWithError(b *testing.B) {
|
||||
io := Left[int](F.Constant[error](nil)())
|
||||
f := func(x int) IOResult[int] { return Of(x * 2) }
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = Chain(f)(io)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkApFirst(b *testing.B) {
|
||||
first := Of(42)
|
||||
second := Of("test")
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ApFirst[int](second)(first)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkApSecond(b *testing.B) {
|
||||
first := Of(42)
|
||||
second := Of("test")
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ApSecond[int](second)(first)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMonadApFirst(b *testing.B) {
|
||||
first := Of(42)
|
||||
second := Of("test")
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = MonadApFirst(first, second)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMonadApSecond(b *testing.B) {
|
||||
first := Of(42)
|
||||
second := Of("test")
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = MonadApSecond(first, second)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFunctor(b *testing.B) {
|
||||
functor := Functor[int, int]()
|
||||
io := Of(42)
|
||||
f := N.Mul(2)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = functor.Map(f)(io)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMonad(b *testing.B) {
|
||||
monad := Monad[int, int]()
|
||||
f := func(x int) IOResult[int] { return Of(x * 2) }
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = monad.Chain(f)(monad.Of(42))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPointed(b *testing.B) {
|
||||
pointed := Pointed[int]()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = pointed.Of(42)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTraverseArray(b *testing.B) {
|
||||
data := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
f := func(x int) IOResult[int] { return Of(x * 2) }
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = TraverseArray(f)(data)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSequenceArray(b *testing.B) {
|
||||
data := []IOResult[int]{Of(1), Of(2), Of(3), Of(4), Of(5)}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = SequenceArray(data)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAlt(b *testing.B) {
|
||||
first := Left[int](F.Constant[error](nil)())
|
||||
second := func() IOResult[int] { return Of(42) }
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = Alt(second)(first)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGetOrElse(b *testing.B) {
|
||||
io := Of(42)
|
||||
def := func(error) func() int { return func() int { return 0 } }
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = GetOrElse(def)(io)()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFold(b *testing.B) {
|
||||
io := Of(42)
|
||||
onLeft := func(error) func() int { return func() int { return 0 } }
|
||||
onRight := func(x int) func() int { return func() int { return x } }
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = Fold(onLeft, onRight)(io)()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFromIO(b *testing.B) {
|
||||
ioVal := func() int { return 42 }
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = FromIO(ioVal)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChainIOK(b *testing.B) {
|
||||
io := Of(42)
|
||||
f := func(x int) func() int { return func() int { return x * 2 } }
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ChainIOK(f)(io)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChainFirst(b *testing.B) {
|
||||
io := Of(42)
|
||||
f := func(x int) IOResult[string] { return Of("test") }
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ChainFirst(f)(io)
|
||||
}
|
||||
}
|
||||
134
v2/idiomatic/ioresult/bind.go
Normal file
134
v2/idiomatic/ioresult/bind.go
Normal file
@@ -0,0 +1,134 @@
|
||||
// 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 ioresult
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/apply"
|
||||
"github.com/IBM/fp-go/v2/internal/chain"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
)
|
||||
|
||||
// Do starts a do-notation computation with an initial state.
|
||||
// This is the entry point for building complex computations using the do-notation style.
|
||||
//
|
||||
//go:inline
|
||||
func Do[S any](
|
||||
empty S,
|
||||
) IOResult[S] {
|
||||
return Of(empty)
|
||||
}
|
||||
|
||||
// Bind adds a computation step in do-notation, extending the state with a new field.
|
||||
// The setter function determines how the new value is added to the state.
|
||||
//
|
||||
//go:inline
|
||||
func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f Kleisli[S1, T],
|
||||
) Operator[S1, S2] {
|
||||
return chain.Bind(
|
||||
Chain[S1, S2],
|
||||
Map[T, S2],
|
||||
setter,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// Let adds a pure transformation step in do-notation.
|
||||
// Unlike Bind, the function does not return an IOResult, making it suitable for pure computations.
|
||||
//
|
||||
//go:inline
|
||||
func Let[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
) Operator[S1, S2] {
|
||||
return functor.Let(
|
||||
Map[S1, S2],
|
||||
setter,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// LetTo adds a constant value to the state in do-notation.
|
||||
func LetTo[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
) Operator[S1, S2] {
|
||||
return functor.LetTo(
|
||||
Map[S1, S2],
|
||||
setter,
|
||||
b,
|
||||
)
|
||||
}
|
||||
|
||||
// BindTo wraps a value in an initial state structure.
|
||||
// This is typically the first operation after creating an IOResult in do-notation.
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
) Operator[T, S1] {
|
||||
return chain.BindTo(
|
||||
Map[T, S1],
|
||||
setter,
|
||||
)
|
||||
}
|
||||
|
||||
// ApS applies an IOResult to extend the state in do-notation.
|
||||
// This is used to add independent computations that don't depend on previous results.
|
||||
func ApS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa IOResult[T],
|
||||
) Operator[S1, S2] {
|
||||
return apply.ApS(
|
||||
Ap[S2, T],
|
||||
Map[S1, func(T) S2],
|
||||
setter,
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// ApSL applies an IOResult using a lens to update a specific field in the state.
|
||||
func ApSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa IOResult[T],
|
||||
) Operator[S, S] {
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// BindL binds a computation using a lens to focus on a specific field.
|
||||
func BindL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Kleisli[T, T],
|
||||
) Operator[S, S] {
|
||||
return Bind(lens.Set, F.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetL applies a pure transformation using a lens to update a specific field.
|
||||
func LetL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f func(T) T,
|
||||
) Operator[S, S] {
|
||||
return Let(lens.Set, F.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetToL sets a field to a constant value using a lens.
|
||||
func LetToL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
b T,
|
||||
) Operator[S, S] {
|
||||
return LetTo(lens.Set, b)
|
||||
}
|
||||
60
v2/idiomatic/ioresult/bind_test.go
Normal file
60
v2/idiomatic/ioresult/bind_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// 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 ioresult
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func getLastName(s utils.Initial) IOResult[string] {
|
||||
return Of("Doe")
|
||||
}
|
||||
|
||||
func getGivenName(s utils.WithLastName) IOResult[string] {
|
||||
return Of("John")
|
||||
}
|
||||
|
||||
func TestBind(t *testing.T) {
|
||||
|
||||
res := F.Pipe3(
|
||||
Do(utils.Empty),
|
||||
Bind(utils.SetLastName, getLastName),
|
||||
Bind(utils.SetGivenName, getGivenName),
|
||||
Map(utils.GetFullName),
|
||||
)
|
||||
|
||||
result, err := res()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "John Doe", result)
|
||||
}
|
||||
|
||||
func TestApS(t *testing.T) {
|
||||
|
||||
res := F.Pipe3(
|
||||
Do(utils.Empty),
|
||||
ApS(utils.SetLastName, Of("Doe")),
|
||||
ApS(utils.SetGivenName, Of("John")),
|
||||
Map(utils.GetFullName),
|
||||
)
|
||||
|
||||
result, err := res()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "John Doe", result)
|
||||
}
|
||||
42
v2/idiomatic/ioresult/bracket.go
Normal file
42
v2/idiomatic/ioresult/bracket.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// 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 ioresult
|
||||
|
||||
import "github.com/IBM/fp-go/v2/idiomatic/result"
|
||||
|
||||
// Bracket makes sure that a resource is cleaned up in the event of an error. The release action is called regardless of
|
||||
// whether the body action returns and error or not.
|
||||
func Bracket[A, B, ANY any](
|
||||
acquire IOResult[A],
|
||||
use Kleisli[A, B],
|
||||
release func(B, error) func(A) IOResult[ANY],
|
||||
) IOResult[B] {
|
||||
return func() (B, error) {
|
||||
a, aerr := acquire()
|
||||
if aerr != nil {
|
||||
return result.Left[B](aerr)
|
||||
}
|
||||
b, berr := use(a)()
|
||||
_, rerr := release(b, berr)(a)()
|
||||
if berr != nil {
|
||||
return result.Left[B](berr)
|
||||
}
|
||||
if rerr != nil {
|
||||
return result.Left[B](rerr)
|
||||
}
|
||||
return result.Of(b)
|
||||
}
|
||||
}
|
||||
302
v2/idiomatic/ioresult/bracket_test.go
Normal file
302
v2/idiomatic/ioresult/bracket_test.go
Normal file
@@ -0,0 +1,302 @@
|
||||
// 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 ioresult
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBracket(t *testing.T) {
|
||||
t.Run("successful acquire, use, and release", func(t *testing.T) {
|
||||
acquired := false
|
||||
used := false
|
||||
released := false
|
||||
|
||||
acquire := func() (string, error) {
|
||||
acquired = true
|
||||
return "resource", nil
|
||||
}
|
||||
|
||||
use := func(r string) IOResult[int] {
|
||||
return func() (int, error) {
|
||||
used = true
|
||||
assert.Equal(t, "resource", r)
|
||||
return 42, nil
|
||||
}
|
||||
}
|
||||
|
||||
release := func(b int, err error) func(string) IOResult[any] {
|
||||
return func(r string) IOResult[any] {
|
||||
return func() (any, error) {
|
||||
released = true
|
||||
assert.Equal(t, "resource", r)
|
||||
assert.Equal(t, 42, b)
|
||||
assert.NoError(t, err)
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := Bracket(acquire, use, release)
|
||||
val, err := result()
|
||||
|
||||
assert.True(t, acquired)
|
||||
assert.True(t, used)
|
||||
assert.True(t, released)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, val)
|
||||
})
|
||||
|
||||
t.Run("acquire fails - use and release not called", func(t *testing.T) {
|
||||
used := false
|
||||
released := false
|
||||
|
||||
acquire := func() (string, error) {
|
||||
return "", errors.New("acquire failed")
|
||||
}
|
||||
|
||||
use := func(r string) IOResult[int] {
|
||||
return func() (int, error) {
|
||||
used = true
|
||||
return 42, nil
|
||||
}
|
||||
}
|
||||
|
||||
release := func(b int, err error) func(string) IOResult[any] {
|
||||
return func(r string) IOResult[any] {
|
||||
return func() (any, error) {
|
||||
released = true
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := Bracket(acquire, use, release)
|
||||
_, err := result()
|
||||
|
||||
assert.False(t, used)
|
||||
assert.False(t, released)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "acquire failed", err.Error())
|
||||
})
|
||||
|
||||
t.Run("use fails - release is still called", func(t *testing.T) {
|
||||
acquired := false
|
||||
released := false
|
||||
|
||||
acquire := func() (string, error) {
|
||||
acquired = true
|
||||
return "resource", nil
|
||||
}
|
||||
|
||||
use := func(r string) IOResult[int] {
|
||||
return func() (int, error) {
|
||||
return 0, errors.New("use failed")
|
||||
}
|
||||
}
|
||||
|
||||
release := func(b int, err error) func(string) IOResult[any] {
|
||||
return func(r string) IOResult[any] {
|
||||
return func() (any, error) {
|
||||
released = true
|
||||
assert.Equal(t, "resource", r)
|
||||
assert.Equal(t, 0, b)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "use failed", err.Error())
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := Bracket(acquire, use, release)
|
||||
_, err := result()
|
||||
|
||||
assert.True(t, acquired)
|
||||
assert.True(t, released)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "use failed", err.Error())
|
||||
})
|
||||
|
||||
t.Run("use succeeds but release fails", func(t *testing.T) {
|
||||
acquired := false
|
||||
used := false
|
||||
released := false
|
||||
|
||||
acquire := func() (string, error) {
|
||||
acquired = true
|
||||
return "resource", nil
|
||||
}
|
||||
|
||||
use := func(r string) IOResult[int] {
|
||||
return func() (int, error) {
|
||||
used = true
|
||||
return 42, nil
|
||||
}
|
||||
}
|
||||
|
||||
release := func(b int, err error) func(string) IOResult[any] {
|
||||
return func(r string) IOResult[any] {
|
||||
return func() (any, error) {
|
||||
released = true
|
||||
return nil, errors.New("release failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := Bracket(acquire, use, release)
|
||||
_, err := result()
|
||||
|
||||
assert.True(t, acquired)
|
||||
assert.True(t, used)
|
||||
assert.True(t, released)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "release failed", err.Error())
|
||||
})
|
||||
|
||||
t.Run("both use and release fail - use error is returned", func(t *testing.T) {
|
||||
acquire := func() (string, error) {
|
||||
return "resource", nil
|
||||
}
|
||||
|
||||
use := func(r string) IOResult[int] {
|
||||
return func() (int, error) {
|
||||
return 0, errors.New("use failed")
|
||||
}
|
||||
}
|
||||
|
||||
release := func(b int, err error) func(string) IOResult[any] {
|
||||
return func(r string) IOResult[any] {
|
||||
return func() (any, error) {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "use failed", err.Error())
|
||||
return nil, errors.New("release failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := Bracket(acquire, use, release)
|
||||
_, err := result()
|
||||
|
||||
assert.Error(t, err)
|
||||
// use error takes precedence
|
||||
assert.Equal(t, "use failed", err.Error())
|
||||
})
|
||||
|
||||
t.Run("resource cleanup with file-like resource", func(t *testing.T) {
|
||||
type File struct {
|
||||
name string
|
||||
closed bool
|
||||
}
|
||||
|
||||
var file *File
|
||||
|
||||
acquire := func() (*File, error) {
|
||||
file = &File{name: "test.txt", closed: false}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
use := func(f *File) IOResult[string] {
|
||||
return func() (string, error) {
|
||||
assert.False(t, f.closed)
|
||||
return "file content", nil
|
||||
}
|
||||
}
|
||||
|
||||
release := func(content string, err error) func(*File) IOResult[any] {
|
||||
return func(f *File) IOResult[any] {
|
||||
return func() (any, error) {
|
||||
f.closed = true
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := Bracket(acquire, use, release)
|
||||
content, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "file content", content)
|
||||
assert.True(t, file.closed)
|
||||
})
|
||||
|
||||
t.Run("release receives both value and error from use", func(t *testing.T) {
|
||||
var receivedValue int
|
||||
var receivedError error
|
||||
|
||||
acquire := func() (string, error) {
|
||||
return "resource", nil
|
||||
}
|
||||
|
||||
use := func(r string) IOResult[int] {
|
||||
return func() (int, error) {
|
||||
return 100, errors.New("use error")
|
||||
}
|
||||
}
|
||||
|
||||
release := func(b int, err error) func(string) IOResult[any] {
|
||||
return func(r string) IOResult[any] {
|
||||
return func() (any, error) {
|
||||
receivedValue = b
|
||||
receivedError = err
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := Bracket(acquire, use, release)
|
||||
_, _ = result()
|
||||
|
||||
assert.Equal(t, 100, receivedValue)
|
||||
assert.Error(t, receivedError)
|
||||
assert.Equal(t, "use error", receivedError.Error())
|
||||
})
|
||||
|
||||
t.Run("release receives zero value and nil when use succeeds", func(t *testing.T) {
|
||||
var receivedValue int
|
||||
var receivedError error
|
||||
|
||||
acquire := func() (string, error) {
|
||||
return "resource", nil
|
||||
}
|
||||
|
||||
use := func(r string) IOResult[int] {
|
||||
return func() (int, error) {
|
||||
return 42, nil
|
||||
}
|
||||
}
|
||||
|
||||
release := func(b int, err error) func(string) IOResult[any] {
|
||||
return func(r string) IOResult[any] {
|
||||
return func() (any, error) {
|
||||
receivedValue = b
|
||||
receivedError = err
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := Bracket(acquire, use, release)
|
||||
val, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, val)
|
||||
assert.Equal(t, 42, receivedValue)
|
||||
assert.NoError(t, receivedError)
|
||||
})
|
||||
}
|
||||
378
v2/idiomatic/ioresult/coverage.out
Normal file
378
v2/idiomatic/ioresult/coverage.out
Normal file
@@ -0,0 +1,378 @@
|
||||
mode: set
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ap.go:23.80,31.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ap.go:34.59,41.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ap.go:44.81,52.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ap.go:55.60,62.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:30.15,32.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:39.20,46.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:53.20,59.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:65.20,71.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:77.19,82.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:89.20,96.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:102.18,104.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:110.18,112.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:118.18,120.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:126.18,128.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:26.15,27.27 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:27.27,29.18 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:29.18,31.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:32.3,34.18 3 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:34.18,36.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:37.3,37.18 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:37.18,39.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:40.3,40.22 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/eq.go:26.74,27.51 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/eq.go:27.51,29.3 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/eq.go:35.58,37.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:12.70,13.28 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:13.28,15.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:18.78,19.33 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:19.33,20.28 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:20.28,22.4 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:26.90,27.40 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:27.40,28.28 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:28.28,30.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:34.102,35.47 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:35.47,36.28 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:36.28,38.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:45.30,50.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:55.30,60.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:65.30,70.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:73.86,78.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:81.89,86.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:89.89,94.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:97.126,98.61 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:98.61,104.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:108.129,109.61 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:109.61,115.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:119.129,120.61 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:120.61,126.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:133.34,140.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:146.34,153.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:159.34,166.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:169.108,175.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:178.111,184.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:187.111,193.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:196.176,197.69 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:197.69,205.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:209.179,210.69 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:210.69,218.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:222.179,223.69 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:223.69,231.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:239.38,248.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:255.38,264.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:271.38,280.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:283.130,290.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:293.133,300.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:303.133,310.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:313.226,314.77 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:314.77,324.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:328.229,329.77 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:329.77,339.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:343.229,344.77 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:344.77,354.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:363.42,374.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:382.42,393.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:401.42,412.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:415.152,423.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:426.155,434.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:437.155,445.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:448.276,449.85 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:449.85,461.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:465.279,466.85 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:466.85,478.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:482.279,483.85 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:483.85,495.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:505.46,518.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:527.46,540.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:549.46,562.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:565.174,574.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:577.177,586.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:589.177,598.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:601.326,602.93 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:602.93,616.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:620.329,621.93 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:621.93,635.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:639.329,640.93 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:640.93,654.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:665.50,680.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:690.50,705.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:715.50,730.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:733.196,743.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:746.199,756.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:759.199,769.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:772.376,773.101 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:773.101,789.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:793.379,794.101 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:794.101,810.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:814.379,815.101 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:815.101,831.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:843.54,860.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:871.54,888.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:899.54,916.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:919.218,930.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:933.221,944.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:947.221,958.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:961.426,962.109 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:962.109,980.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:984.429,985.109 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:985.109,1003.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1007.429,1008.109 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1008.109,1026.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1039.58,1058.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1070.58,1089.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1101.58,1120.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1123.240,1135.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1138.243,1150.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1153.243,1165.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1168.476,1169.117 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1169.117,1189.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1193.479,1194.117 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1194.117,1214.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1218.479,1219.117 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1219.117,1239.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1253.62,1274.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1287.62,1308.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1321.62,1342.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1345.262,1358.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1361.265,1374.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1377.265,1390.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1393.526,1394.125 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1394.125,1416.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1420.529,1421.125 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1421.125,1443.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1447.529,1448.125 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1448.125,1470.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1485.68,1508.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1522.68,1545.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1559.68,1582.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1585.290,1599.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1602.293,1616.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1619.293,1633.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1636.588,1637.137 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1637.137,1661.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1665.591,1666.137 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1666.137,1690.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1694.591,1695.137 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1695.137,1719.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:32.39,33.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:33.27,35.3 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:40.36,41.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:41.27,43.3 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:47.33,49.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:52.38,54.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:58.46,59.31 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:59.31,62.3 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:67.43,68.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:68.27,70.3 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:75.49,76.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:76.27,78.3 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:83.52,84.27 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:84.27,86.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:92.70,93.40 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:93.40,94.28 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:94.28,95.10 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:95.10,97.5 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:98.4,98.35 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:105.88,106.50 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:106.50,107.42 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:107.42,108.29 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:108.29,110.19 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:110.19,112.6 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:113.5,114.11 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:114.11,116.6 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:117.5,117.36 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:125.78,126.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:126.27,128.17 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:128.17,130.4 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:131.3,131.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:136.60,138.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:141.61,143.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:146.42,148.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:151.46,153.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:157.66,158.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:158.27,160.17 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:160.17,162.4 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:163.3,163.25 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:169.48,171.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:174.60,176.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:179.42,181.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:185.72,186.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:186.27,188.17 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:188.17,190.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:191.3,191.16 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:198.54,200.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:204.93,205.27 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:205.27,207.17 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:207.17,209.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:210.3,210.29 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:215.75,217.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:221.86,222.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:222.27,224.17 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:224.17,226.4 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:227.3,227.14 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:232.68,234.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:237.77,239.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:242.58,244.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:248.80,249.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:249.27,256.13 5 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:256.13,259.4 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:261.3,264.20 3 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:264.20,266.4 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:267.3,267.19 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:267.19,269.4 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:271.3,271.28 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:276.61,278.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:282.80,283.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:283.27,285.17 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:285.17,287.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:289.3,290.17 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:290.17,292.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:294.3,294.28 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:299.76,301.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:304.60,306.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:310.49,316.16 4 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:316.16,318.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:320.2,320.27 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:320.27,323.3 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:328.77,329.27 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:329.27,331.17 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:331.17,333.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:334.3,334.22 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:339.59,341.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:344.91,345.27 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:345.27,347.17 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:347.17,349.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:350.3,350.25 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:355.73,357.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:360.97,362.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:366.73,367.36 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:367.36,368.19 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:368.19,370.18 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:370.18,372.5 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:373.4,373.12 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:379.73,381.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:384.55,386.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:390.77,391.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:391.27,393.17 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:393.17,395.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:396.3,397.17 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:397.17,399.4 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:400.3,400.22 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:405.70,407.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:410.59,412.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:415.52,417.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:419.98,420.27 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:420.27,422.17 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:422.17,424.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:425.3,426.17 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:426.17,428.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:429.3,429.22 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:434.80,436.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:438.91,439.27 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:439.27,441.17 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:441.17,443.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:444.3,445.17 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:445.17,447.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:448.3,448.22 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:453.73,455.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:458.83,459.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:459.27,461.17 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:461.17,463.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:464.3,465.22 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:470.65,472.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:475.91,477.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:480.73,482.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:485.84,487.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:490.66,492.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:495.76,497.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:500.58,502.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:506.100,507.18 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:507.18,509.17 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:509.17,511.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:512.3,512.22 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:523.29,524.43 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:524.43,525.28 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:525.28,527.19 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:527.19,529.5 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:530.4,532.19 3 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:532.19,534.5 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:535.4,535.19 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:535.19,537.5 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:538.4,538.23 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:545.41,546.29 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:546.29,549.3 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:554.54,555.27 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:555.27,557.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:562.79,563.27 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:563.27,565.17 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:565.17,567.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:568.3,568.22 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:573.58,575.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:578.68,580.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:583.49,585.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:590.55,591.42 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:591.42,592.28 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:592.28,595.4 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:602.55,603.42 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:603.42,604.28 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:604.28,607.33 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:607.33,609.5 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:610.4,610.15 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:617.77,618.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:618.27,620.17 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:620.17,622.4 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:623.3,623.22 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:628.59,630.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:634.85,635.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:635.27,637.17 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:637.17,640.4 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:641.3,641.22 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:646.78,648.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:650.67,652.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:655.60,657.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/logging.go:27.52,28.33 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/logging.go:28.33,32.30 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/logging.go:32.30,33.22 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/logging.go:33.22,35.5 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/logging.go:36.4,37.28 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:33.50,35.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:38.51,40.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:43.63,45.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:48.69,50.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:53.73,55.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:58.65,60.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:65.55,67.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:72.74,74.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:79.89,81.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/monoid.go:31.13,38.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/monoid.go:45.13,52.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/monoid.go:59.13,66.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/retry.go:31.15,32.73 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/retry.go:32.73,33.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/retry.go:33.27,35.4 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/retry.go:37.26,39.4 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/retry.go:40.2,40.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/retry.go:40.27,42.3 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/semigroup.go:29.41,33.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/sync.go:25.66,26.42 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/sync.go:26.42,27.28 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/sync.go:27.28,30.4 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:27.65,35.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:40.85,48.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:53.59,55.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:60.88,68.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:73.106,81.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:86.82,88.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:93.68,101.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:105.88,113.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:117.62,119.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:123.91,131.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:135.109,143.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:147.85,149.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:154.68,162.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:166.88,174.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:178.62,180.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:184.91,192.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:196.109,204.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:208.85,210.2 1 0
|
||||
378
v2/idiomatic/ioresult/coverage_new.out
Normal file
378
v2/idiomatic/ioresult/coverage_new.out
Normal file
@@ -0,0 +1,378 @@
|
||||
mode: set
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ap.go:23.80,31.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ap.go:34.59,41.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ap.go:44.81,52.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ap.go:55.60,62.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:30.15,32.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:39.20,46.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:53.20,59.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:65.20,71.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:77.19,82.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:89.20,96.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:102.18,104.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:110.18,112.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:118.18,120.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:126.18,128.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:26.15,27.27 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:27.27,29.18 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:29.18,31.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:32.3,34.18 3 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:34.18,36.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:37.3,37.18 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:37.18,39.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:40.3,40.22 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/eq.go:26.74,27.51 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/eq.go:27.51,29.3 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/eq.go:35.58,37.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:12.70,13.28 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:13.28,15.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:18.78,19.33 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:19.33,20.28 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:20.28,22.4 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:26.90,27.40 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:27.40,28.28 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:28.28,30.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:34.102,35.47 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:35.47,36.28 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:36.28,38.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:45.30,50.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:55.30,60.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:65.30,70.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:73.86,78.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:81.89,86.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:89.89,94.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:97.126,98.61 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:98.61,104.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:108.129,109.61 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:109.61,115.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:119.129,120.61 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:120.61,126.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:133.34,140.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:146.34,153.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:159.34,166.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:169.108,175.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:178.111,184.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:187.111,193.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:196.176,197.69 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:197.69,205.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:209.179,210.69 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:210.69,218.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:222.179,223.69 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:223.69,231.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:239.38,248.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:255.38,264.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:271.38,280.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:283.130,290.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:293.133,300.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:303.133,310.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:313.226,314.77 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:314.77,324.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:328.229,329.77 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:329.77,339.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:343.229,344.77 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:344.77,354.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:363.42,374.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:382.42,393.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:401.42,412.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:415.152,423.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:426.155,434.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:437.155,445.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:448.276,449.85 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:449.85,461.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:465.279,466.85 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:466.85,478.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:482.279,483.85 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:483.85,495.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:505.46,518.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:527.46,540.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:549.46,562.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:565.174,574.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:577.177,586.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:589.177,598.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:601.326,602.93 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:602.93,616.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:620.329,621.93 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:621.93,635.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:639.329,640.93 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:640.93,654.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:665.50,680.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:690.50,705.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:715.50,730.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:733.196,743.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:746.199,756.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:759.199,769.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:772.376,773.101 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:773.101,789.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:793.379,794.101 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:794.101,810.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:814.379,815.101 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:815.101,831.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:843.54,860.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:871.54,888.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:899.54,916.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:919.218,930.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:933.221,944.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:947.221,958.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:961.426,962.109 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:962.109,980.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:984.429,985.109 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:985.109,1003.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1007.429,1008.109 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1008.109,1026.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1039.58,1058.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1070.58,1089.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1101.58,1120.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1123.240,1135.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1138.243,1150.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1153.243,1165.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1168.476,1169.117 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1169.117,1189.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1193.479,1194.117 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1194.117,1214.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1218.479,1219.117 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1219.117,1239.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1253.62,1274.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1287.62,1308.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1321.62,1342.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1345.262,1358.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1361.265,1374.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1377.265,1390.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1393.526,1394.125 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1394.125,1416.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1420.529,1421.125 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1421.125,1443.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1447.529,1448.125 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1448.125,1470.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1485.68,1508.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1522.68,1545.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1559.68,1582.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1585.290,1599.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1602.293,1616.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1619.293,1633.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1636.588,1637.137 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1637.137,1661.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1665.591,1666.137 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1666.137,1690.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1694.591,1695.137 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1695.137,1719.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:32.39,33.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:33.27,35.3 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:40.36,41.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:41.27,43.3 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:47.33,49.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:52.38,54.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:58.46,59.31 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:59.31,62.3 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:67.43,68.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:68.27,70.3 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:75.49,76.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:76.27,78.3 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:83.52,84.27 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:84.27,86.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:92.70,93.40 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:93.40,94.28 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:94.28,95.10 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:95.10,97.5 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:98.4,98.35 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:105.88,106.50 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:106.50,107.42 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:107.42,108.29 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:108.29,110.19 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:110.19,112.6 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:113.5,114.11 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:114.11,116.6 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:117.5,117.36 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:125.78,126.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:126.27,128.17 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:128.17,130.4 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:131.3,131.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:136.60,138.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:141.61,143.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:146.42,148.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:151.46,153.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:157.66,158.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:158.27,160.17 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:160.17,162.4 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:163.3,163.25 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:169.48,171.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:174.60,176.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:179.42,181.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:185.72,186.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:186.27,188.17 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:188.17,190.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:191.3,191.16 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:198.54,200.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:204.93,205.27 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:205.27,207.17 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:207.17,209.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:210.3,210.29 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:215.75,217.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:221.86,222.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:222.27,224.17 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:224.17,226.4 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:227.3,227.14 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:232.68,234.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:237.77,239.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:242.58,244.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:248.80,249.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:249.27,256.13 5 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:256.13,259.4 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:261.3,264.20 3 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:264.20,266.4 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:267.3,267.19 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:267.19,269.4 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:271.3,271.28 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:276.61,278.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:282.80,283.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:283.27,285.17 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:285.17,287.4 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:289.3,290.17 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:290.17,292.4 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:294.3,294.28 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:299.76,301.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:304.60,306.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:310.49,316.16 4 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:316.16,318.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:320.2,320.27 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:320.27,323.3 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:328.77,329.27 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:329.27,331.17 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:331.17,333.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:334.3,334.22 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:339.59,341.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:344.91,345.27 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:345.27,347.17 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:347.17,349.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:350.3,350.25 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:355.73,357.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:360.97,362.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:366.73,367.36 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:367.36,368.19 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:368.19,370.18 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:370.18,372.5 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:373.4,373.12 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:379.73,381.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:384.55,386.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:390.77,391.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:391.27,393.17 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:393.17,395.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:396.3,397.17 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:397.17,399.4 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:400.3,400.22 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:405.70,407.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:410.59,412.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:415.52,417.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:419.98,420.27 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:420.27,422.17 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:422.17,424.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:425.3,426.17 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:426.17,428.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:429.3,429.22 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:434.80,436.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:438.91,439.27 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:439.27,441.17 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:441.17,443.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:444.3,445.17 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:445.17,447.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:448.3,448.22 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:453.73,455.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:458.83,459.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:459.27,461.17 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:461.17,463.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:464.3,465.22 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:470.65,472.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:475.91,477.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:480.73,482.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:485.84,487.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:490.66,492.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:495.76,497.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:500.58,502.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:506.100,507.18 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:507.18,509.17 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:509.17,511.4 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:512.3,512.22 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:523.29,524.43 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:524.43,525.28 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:525.28,527.19 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:527.19,529.5 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:530.4,532.19 3 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:532.19,534.5 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:535.4,535.19 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:535.19,537.5 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:538.4,538.23 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:545.41,546.29 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:546.29,549.3 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:554.54,555.27 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:555.27,557.3 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:562.79,563.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:563.27,565.17 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:565.17,567.4 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:568.3,568.22 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:573.58,575.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:578.68,580.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:583.49,585.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:590.55,591.42 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:591.42,592.28 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:592.28,595.4 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:602.55,603.42 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:603.42,604.28 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:604.28,607.33 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:607.33,609.5 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:610.4,610.15 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:617.77,618.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:618.27,620.17 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:620.17,622.4 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:623.3,623.22 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:628.59,630.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:634.85,635.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:635.27,637.17 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:637.17,640.4 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:641.3,641.22 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:646.78,648.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:650.67,652.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:655.60,657.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/logging.go:27.52,28.33 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/logging.go:28.33,32.30 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/logging.go:32.30,33.22 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/logging.go:33.22,35.5 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/logging.go:36.4,37.28 2 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:33.50,35.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:38.51,40.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:43.63,45.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:48.69,50.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:53.73,55.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:58.65,60.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:65.55,67.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:72.74,74.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:79.89,81.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/monoid.go:31.13,38.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/monoid.go:45.13,52.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/monoid.go:59.13,66.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/retry.go:31.15,32.73 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/retry.go:32.73,33.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/retry.go:33.27,35.4 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/retry.go:37.26,39.4 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/retry.go:40.2,40.27 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/retry.go:40.27,42.3 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/semigroup.go:29.41,33.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/sync.go:25.66,26.42 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/sync.go:26.42,27.28 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/sync.go:27.28,30.4 2 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:27.65,35.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:40.85,48.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:53.59,55.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:60.88,68.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:73.106,81.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:86.82,88.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:93.68,101.2 1 1
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:105.88,113.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:117.62,119.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:123.91,131.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:135.109,143.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:147.85,149.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:154.68,162.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:166.88,174.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:178.62,180.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:184.91,192.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:196.109,204.2 1 0
|
||||
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:208.85,210.2 1 0
|
||||
198
v2/idiomatic/ioresult/doc.go
Normal file
198
v2/idiomatic/ioresult/doc.go
Normal file
@@ -0,0 +1,198 @@
|
||||
// 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 ioresult provides functional programming combinators for working with IO operations
|
||||
// that can fail with errors, following Go's idiomatic (value, error) tuple pattern.
|
||||
//
|
||||
// # Overview
|
||||
//
|
||||
// IOResult[A] represents a computation that performs IO and returns either a value of type A
|
||||
// or an error. It is defined as:
|
||||
//
|
||||
// type IOResult[A any] = func() (A, error)
|
||||
//
|
||||
// This is the idiomatic Go version of IOEither, using Go's standard error handling pattern
|
||||
// instead of the Either monad. It combines:
|
||||
// - IO effects (functions that perform side effects)
|
||||
// - Error handling via Go's (value, error) tuple return pattern
|
||||
//
|
||||
// # Why Parameterless Functions Represent IO Operations
|
||||
//
|
||||
// The key insight behind IOResult is that a function returning a value without taking any
|
||||
// input can only produce that value through side effects. Consider:
|
||||
//
|
||||
// func() int { return 42 } // Pure: always returns 42
|
||||
// func() int { return readFromFile() } // Impure: result depends on external state
|
||||
//
|
||||
// When a parameterless function returns different values on different calls, or when it
|
||||
// interacts with the outside world (filesystem, network, random number generator, current
|
||||
// time, database), it is performing a side effect - an observable interaction with state
|
||||
// outside the function's scope.
|
||||
//
|
||||
// # Lazy Evaluation and Referential Transparency
|
||||
//
|
||||
// IOResult provides two critical benefits:
|
||||
//
|
||||
// 1. **Lazy Evaluation**: The side effect doesn't happen when you create the IOResult,
|
||||
// only when you call it (execute it). This allows you to build complex computations
|
||||
// as pure data structures and defer execution until needed.
|
||||
//
|
||||
// // This doesn't read the file yet, just describes how to read it
|
||||
// readConfig := func() (Config, error) { return os.ReadFile("config.json") }
|
||||
//
|
||||
// // Still hasn't read the file, just composed operations
|
||||
// parsed := Map(parseJSON)(readConfig)
|
||||
//
|
||||
// // NOW it reads the file and parses it
|
||||
// config, err := parsed()
|
||||
//
|
||||
// 2. **Referential Transparency of the Description**: While the IO operation itself has
|
||||
// side effects, the IOResult value (the function) is referentially transparent. You can
|
||||
// pass it around, compose it, and reason about it without triggering the side effect.
|
||||
// The side effect only occurs when you explicitly call the function.
|
||||
//
|
||||
// # Distinguishing Pure from Impure Operations
|
||||
//
|
||||
// The type system makes the distinction clear:
|
||||
//
|
||||
// // Pure function: always returns the same output for the same input
|
||||
// func double(x int) int { return x * 2 }
|
||||
//
|
||||
// // Impure operation: encapsulated in IOResult
|
||||
// func readFile(path string) IOResult[[]byte] {
|
||||
// return func() ([]byte, error) {
|
||||
// return os.ReadFile(path) // Side effect: file system access
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// The IOResult type explicitly marks operations as having side effects, making the
|
||||
// distinction between pure and impure code visible in the type system. This allows
|
||||
// developers to:
|
||||
// - Identify which parts of the code interact with external state
|
||||
// - Test pure logic separately from IO operations
|
||||
// - Compose IO operations while keeping them lazy
|
||||
// - Control when and where side effects occur
|
||||
//
|
||||
// # Examples of Side Effects Captured by IOResult
|
||||
//
|
||||
// IOResult is appropriate for operations that:
|
||||
// - Read from or write to files, databases, or network
|
||||
// - Generate random numbers
|
||||
// - Read the current time
|
||||
// - Modify mutable state
|
||||
// - Interact with external APIs
|
||||
// - Execute system commands
|
||||
// - Acquire or release resources
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Each call potentially returns a different value
|
||||
// getCurrentTime := func() (time.Time, error) {
|
||||
// return time.Now(), nil // Side effect: reads system clock
|
||||
// }
|
||||
//
|
||||
// // Each call reads from external state
|
||||
// readDatabase := func() (User, error) {
|
||||
// return db.Query("SELECT * FROM users WHERE id = ?", 1)
|
||||
// }
|
||||
//
|
||||
// // Composes multiple IO operations
|
||||
// pipeline := F.Pipe2(
|
||||
// getCurrentTime,
|
||||
// Chain(func(t time.Time) IOResult[string] {
|
||||
// return func() (string, error) {
|
||||
// return fmt.Sprintf("Time: %v", t), nil
|
||||
// }
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
// # Core Concepts
|
||||
//
|
||||
// IOResult follows functional programming principles:
|
||||
// - Functor: Transform successful values with Map
|
||||
// - Applicative: Combine multiple IOResults with Ap, ApS
|
||||
// - Monad: Chain dependent computations with Chain, Bind
|
||||
// - Error recovery: Handle errors with ChainLeft, Alt
|
||||
//
|
||||
// # Basic Usage
|
||||
//
|
||||
// Creating IOResult values:
|
||||
//
|
||||
// success := Of(42) // Right value
|
||||
// failure := Left[int](errors.New("error")) // Left (error) value
|
||||
//
|
||||
// Transforming values:
|
||||
//
|
||||
// doubled := Map(N.Mul(2))(success)
|
||||
//
|
||||
// Chaining computations:
|
||||
//
|
||||
// result := Chain(func(x int) IOResult[string] {
|
||||
// return Of(fmt.Sprintf("%d", x))
|
||||
// })(success)
|
||||
//
|
||||
// # Do Notation
|
||||
//
|
||||
// The package supports do-notation style composition for building complex computations:
|
||||
//
|
||||
// result := F.Pipe5(
|
||||
// Of("John"),
|
||||
// BindTo(T.Of[string]),
|
||||
// ApS(T.Push1[string, int], Of(42)),
|
||||
// Bind(T.Push2[string, int, string], func(t T.Tuple2[string, int]) IOResult[string] {
|
||||
// return Of(fmt.Sprintf("%s: %d", t.F1, t.F2))
|
||||
// }),
|
||||
// Map(transform),
|
||||
// )
|
||||
//
|
||||
// # Error Handling
|
||||
//
|
||||
// IOResult provides several ways to handle errors:
|
||||
// - ChainLeft: Transform error values into new computations
|
||||
// - Alt: Provide alternative computations when an error occurs
|
||||
// - GetOrElse: Extract values with a default for errors
|
||||
// - Fold: Handle both success and error cases explicitly
|
||||
//
|
||||
// # Concurrency
|
||||
//
|
||||
// IOResult supports both sequential and parallel execution:
|
||||
// - ApSeq, TraverseArraySeq: Sequential execution
|
||||
// - ApPar, TraverseArrayPar: Parallel execution (default)
|
||||
// - Ap, TraverseArray: Defaults to parallel execution
|
||||
//
|
||||
// # Resource Management
|
||||
//
|
||||
// The package provides resource management utilities:
|
||||
// - Bracket: Acquire, use, and release resources safely
|
||||
// - WithResource: Scoped resource management
|
||||
// - WithLock: Execute operations within a lock scope
|
||||
//
|
||||
// # Conversion Functions
|
||||
//
|
||||
// IOResult interoperates with other types:
|
||||
// - FromEither: Convert Either to IOResult
|
||||
// - FromResult: Convert (value, error) tuple to IOResult
|
||||
// - FromOption: Convert Option to IOResult
|
||||
// - FromIO: Convert pure IO to IOResult (always succeeds)
|
||||
//
|
||||
// # Examples
|
||||
//
|
||||
// See the example tests for detailed usage patterns:
|
||||
// - examples_create_test.go: Creating IOResult values
|
||||
// - examples_do_test.go: Using do-notation
|
||||
// - examples_extract_test.go: Extracting values from IOResult
|
||||
package ioresult
|
||||
|
||||
//go:generate go run .. ioeither --count 10 --filename gen.go
|
||||
37
v2/idiomatic/ioresult/eq.go
Normal file
37
v2/idiomatic/ioresult/eq.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// 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 ioresult
|
||||
|
||||
import (
|
||||
EQ "github.com/IBM/fp-go/v2/eq"
|
||||
"github.com/IBM/fp-go/v2/idiomatic/result"
|
||||
)
|
||||
|
||||
// Eq implements the equals predicate for values contained in the IOEither monad
|
||||
// Eq constructs an equality predicate for IOResult values.
|
||||
// The comparison function receives (value, error) tuples from both IOResults.
|
||||
func Eq[A any](eq func(A, error) func(A, error) bool) EQ.Eq[IOResult[A]] {
|
||||
return EQ.FromEquals(func(l, r IOResult[A]) bool {
|
||||
return eq(l())(r())
|
||||
})
|
||||
}
|
||||
|
||||
// FromStrictEquals constructs an [EQ.Eq] from the canonical comparison function
|
||||
// FromStrictEquals constructs an Eq from Go's built-in equality (==) for comparable types.
|
||||
// Both the value and error must match for two IOResults to be considered equal.
|
||||
func FromStrictEquals[A comparable]() EQ.Eq[IOResult[A]] {
|
||||
return Eq(result.FromStrictEquals[A]())
|
||||
}
|
||||
47
v2/idiomatic/ioresult/eq_test.go
Normal file
47
v2/idiomatic/ioresult/eq_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// 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 ioresult
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEq(t *testing.T) {
|
||||
|
||||
r1 := Of(1)
|
||||
r2 := Of(1)
|
||||
r3 := Of(2)
|
||||
|
||||
err1 := errors.New("a")
|
||||
e1 := Left[int](err1)
|
||||
e2 := Left[int](err1) // Same error instance
|
||||
e3 := Left[int](errors.New("b"))
|
||||
|
||||
eq := FromStrictEquals[int]()
|
||||
|
||||
assert.True(t, eq.Equals(r1, r1))
|
||||
assert.True(t, eq.Equals(r1, r2))
|
||||
assert.False(t, eq.Equals(r1, r3))
|
||||
assert.False(t, eq.Equals(r1, e1))
|
||||
|
||||
assert.True(t, eq.Equals(e1, e1))
|
||||
assert.True(t, eq.Equals(e1, e2))
|
||||
assert.False(t, eq.Equals(e1, e3))
|
||||
assert.False(t, eq.Equals(e2, r2))
|
||||
}
|
||||
87
v2/idiomatic/ioresult/examples_create_test.go
Normal file
87
v2/idiomatic/ioresult/examples_create_test.go
Normal file
@@ -0,0 +1,87 @@
|
||||
// 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 ioresult
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
)
|
||||
|
||||
func ExampleIOResult_creation() {
|
||||
// Build an IOResult
|
||||
leftValue := Left[string](fmt.Errorf("some error"))
|
||||
rightValue := Right("value")
|
||||
|
||||
// Convert from Either
|
||||
eitherValue := E.Of[error](42)
|
||||
ioFromEither := FromEither(eitherValue)
|
||||
|
||||
// some predicate
|
||||
isEven := func(num int) (int, error) {
|
||||
if num%2 == 0 {
|
||||
return num, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%d is an odd number", num)
|
||||
}
|
||||
fromEven := Eitherize1(isEven)
|
||||
leftFromPred := fromEven(3)
|
||||
rightFromPred := fromEven(4)
|
||||
|
||||
// Convert results to Either for display
|
||||
val1, err1 := leftValue()
|
||||
if err1 != nil {
|
||||
fmt.Printf("Left[*errors.errorString](%s)\n", err1.Error())
|
||||
} else {
|
||||
fmt.Printf("Right[string](%s)\n", val1)
|
||||
}
|
||||
|
||||
val2, err2 := rightValue()
|
||||
if err2 != nil {
|
||||
fmt.Printf("Left[*errors.errorString](%s)\n", err2.Error())
|
||||
} else {
|
||||
fmt.Printf("Right[string](%s)\n", val2)
|
||||
}
|
||||
|
||||
val3, err3 := ioFromEither()
|
||||
if err3 != nil {
|
||||
fmt.Printf("Left[*errors.errorString](%s)\n", err3.Error())
|
||||
} else {
|
||||
fmt.Printf("Right[int](%d)\n", val3)
|
||||
}
|
||||
|
||||
val4, err4 := leftFromPred()
|
||||
if err4 != nil {
|
||||
fmt.Printf("Left[*errors.errorString](%s)\n", err4.Error())
|
||||
} else {
|
||||
fmt.Printf("Right[int](%d)\n", val4)
|
||||
}
|
||||
|
||||
val5, err5 := rightFromPred()
|
||||
if err5 != nil {
|
||||
fmt.Printf("Left[*errors.errorString](%s)\n", err5.Error())
|
||||
} else {
|
||||
fmt.Printf("Right[int](%d)\n", val5)
|
||||
}
|
||||
|
||||
// Output:
|
||||
// Left[*errors.errorString](some error)
|
||||
// Right[string](value)
|
||||
// Right[int](42)
|
||||
// Left[*errors.errorString](3 is an odd number)
|
||||
// Right[int](4)
|
||||
|
||||
}
|
||||
62
v2/idiomatic/ioresult/examples_do_test.go
Normal file
62
v2/idiomatic/ioresult/examples_do_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// 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 ioresult
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
)
|
||||
|
||||
func ExampleIOResult_do() {
|
||||
foo := Of("foo")
|
||||
bar := Of(1)
|
||||
|
||||
// quux consumes the state of three bindings and returns an [IO] instead of an [IOResult]
|
||||
quux := func(t T.Tuple3[string, int, string]) IO[any] {
|
||||
return io.FromImpure(func() {
|
||||
log.Printf("t1: %s, t2: %d, t3: %s", t.F1, t.F2, t.F3)
|
||||
})
|
||||
}
|
||||
|
||||
transform := func(t T.Tuple3[string, int, string]) int {
|
||||
return len(t.F1) + t.F2 + len(t.F3)
|
||||
}
|
||||
|
||||
b := F.Pipe5(
|
||||
foo,
|
||||
BindTo(T.Of[string]),
|
||||
ApS(T.Push1[string, int], bar),
|
||||
Bind(T.Push2[string, int, string], func(t T.Tuple2[string, int]) IOResult[string] {
|
||||
return Of(fmt.Sprintf("%s%d", t.F1, t.F2))
|
||||
}),
|
||||
ChainFirstIOK(quux),
|
||||
Map(transform),
|
||||
)
|
||||
|
||||
val, err := b()
|
||||
if err != nil {
|
||||
fmt.Printf("Left[error](%s)\n", err.Error())
|
||||
} else {
|
||||
fmt.Printf("Right[int](%d)\n", val)
|
||||
}
|
||||
|
||||
// Output:
|
||||
// Right[int](8)
|
||||
}
|
||||
46
v2/idiomatic/ioresult/examples_extract_test.go
Normal file
46
v2/idiomatic/ioresult/examples_extract_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// 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 ioresult
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
)
|
||||
|
||||
func ExampleIOResult_extraction() {
|
||||
// IOResult
|
||||
someIOResult := Right(42)
|
||||
value, err := someIOResult() // (42, nil)
|
||||
if err == nil {
|
||||
fmt.Println(E.Of[error](value)) // Convert to Either for display
|
||||
}
|
||||
|
||||
// Or more directly using GetOrElse
|
||||
infallibleIO := GetOrElse(F.Constant1[error](io.Of(0)))(someIOResult) // => io returns 42
|
||||
valueFromIO := infallibleIO() // => 42
|
||||
|
||||
fmt.Println(value)
|
||||
fmt.Println(valueFromIO)
|
||||
|
||||
// Output:
|
||||
// Right[int](42)
|
||||
// 42
|
||||
// 42
|
||||
|
||||
}
|
||||
152
v2/idiomatic/ioresult/exec/exec.go
Normal file
152
v2/idiomatic/ioresult/exec/exec.go
Normal file
@@ -0,0 +1,152 @@
|
||||
// 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 exec provides utilities for executing system commands with IOResult-based error handling.
|
||||
//
|
||||
// This package wraps system command execution in the IOResult monad, which represents
|
||||
// IO operations that can fail with errors using Go's idiomatic (value, error) tuple pattern.
|
||||
// Unlike the result/exec package, these operations explicitly acknowledge their side-effectful
|
||||
// nature by returning IOResult instead of plain Result.
|
||||
//
|
||||
// # Overview
|
||||
//
|
||||
// The exec package is designed for executing system commands as IO operations that may fail.
|
||||
// Since command execution is inherently side-effectful (it interacts with the operating system,
|
||||
// may produce different results over time, and has observable effects), IOResult is the
|
||||
// appropriate abstraction.
|
||||
//
|
||||
// # Basic Usage
|
||||
//
|
||||
// The primary function is Command, which executes a system command:
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/bytes"
|
||||
// "github.com/IBM/fp-go/v2/exec"
|
||||
// "github.com/IBM/fp-go/v2/function"
|
||||
// "github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
// ioexec "github.com/IBM/fp-go/v2/idiomatic/ioresult/exec"
|
||||
// )
|
||||
//
|
||||
// // Execute a command and get the output
|
||||
// version := F.Pipe1(
|
||||
// ioexec.Command("openssl")([]string{"version"})([]byte{}),
|
||||
// ioresult.Map(F.Flow2(
|
||||
// exec.StdOut,
|
||||
// bytes.ToString,
|
||||
// )),
|
||||
// )
|
||||
//
|
||||
// // Run the IO operation
|
||||
// result, err := version()
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// fmt.Println(result)
|
||||
//
|
||||
// # Command Output
|
||||
//
|
||||
// Commands return exec.CommandOutput, which contains both stdout and stderr as byte slices.
|
||||
// Use exec.StdOut and exec.StdErr to extract the respective streams:
|
||||
//
|
||||
// output := ioexec.Command("ls")([]string{"-la"})([]byte{})
|
||||
// result := ioresult.Map(func(out exec.CommandOutput) string {
|
||||
// stdout := exec.StdOut(out)
|
||||
// stderr := exec.StdErr(out)
|
||||
// return bytes.ToString(stdout)
|
||||
// })(output)
|
||||
//
|
||||
// # Composing Commands
|
||||
//
|
||||
// Commands can be composed using IOResult combinators:
|
||||
//
|
||||
// // Chain multiple commands together
|
||||
// pipeline := F.Pipe2(
|
||||
// ioexec.Command("echo")([]string{"hello world"})([]byte{}),
|
||||
// ioresult.Chain(func(out exec.CommandOutput) ioexec.IOResult[exec.CommandOutput] {
|
||||
// input := exec.StdOut(out)
|
||||
// return ioexec.Command("tr")([]string{"a-z", "A-Z"})(input)
|
||||
// }),
|
||||
// ioresult.Map(F.Flow2(exec.StdOut, bytes.ToString)),
|
||||
// )
|
||||
//
|
||||
// # Error Handling
|
||||
//
|
||||
// Commands return errors for various failure conditions:
|
||||
// - Command not found
|
||||
// - Non-zero exit status
|
||||
// - Permission errors
|
||||
// - System resource errors
|
||||
//
|
||||
// Handle errors using IOResult's error handling combinators:
|
||||
//
|
||||
// safeCommand := F.Pipe1(
|
||||
// ioexec.Command("risky-command")([]string{})([]byte{}),
|
||||
// ioresult.Alt(func() (exec.CommandOutput, error) {
|
||||
// // Fallback on error
|
||||
// return exec.CommandOutput{}, nil
|
||||
// }),
|
||||
// )
|
||||
package exec
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/exec"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
INTE "github.com/IBM/fp-go/v2/internal/exec"
|
||||
)
|
||||
|
||||
var (
|
||||
// Command executes a system command with side effects and returns an IOResult.
|
||||
//
|
||||
// This function is curried to allow partial application. It takes three parameters:
|
||||
// - name: The command name or path to execute
|
||||
// - args: Command-line arguments as a slice of strings
|
||||
// - in: Input bytes to send to the command's stdin
|
||||
//
|
||||
// Returns IOResult[exec.CommandOutput] which, when executed, will run the command
|
||||
// and return either the command output or an error.
|
||||
//
|
||||
// The command is executed using the system's default shell context. The output
|
||||
// contains both stdout and stderr as byte slices, accessible via exec.StdOut
|
||||
// and exec.StdErr respectively.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Simple command execution
|
||||
// version := Command("node")([]string{"--version"})([]byte{})
|
||||
// result, err := version()
|
||||
//
|
||||
// // With input piped to stdin
|
||||
// echo := Command("cat")([]string{})([]byte("hello world"))
|
||||
//
|
||||
// // Partial application for reuse
|
||||
// git := Command("git")
|
||||
// status := git([]string{"status"})([]byte{})
|
||||
// log := git([]string{"log", "--oneline"})([]byte{})
|
||||
//
|
||||
// // Composed with IOResult combinators
|
||||
// result := F.Pipe1(
|
||||
// Command("openssl")([]string{"version"})([]byte{}),
|
||||
// ioresult.Map(F.Flow2(exec.StdOut, bytes.ToString)),
|
||||
// )
|
||||
Command = function.Curry3(command)
|
||||
)
|
||||
|
||||
func command(name string, args []string, in []byte) IOResult[exec.CommandOutput] {
|
||||
return func() (exec.CommandOutput, error) {
|
||||
return INTE.Exec(context.Background(), name, args, in)
|
||||
}
|
||||
}
|
||||
44
v2/idiomatic/ioresult/exec/exec_test.go
Normal file
44
v2/idiomatic/ioresult/exec/exec_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2023 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 exec
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
RA "github.com/IBM/fp-go/v2/array"
|
||||
B "github.com/IBM/fp-go/v2/bytes"
|
||||
"github.com/IBM/fp-go/v2/exec"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOpenSSL(t *testing.T) {
|
||||
// execute the openSSL binary
|
||||
version := F.Pipe1(
|
||||
Command("openssl")(RA.From("version"))(B.Monoid.Empty()),
|
||||
ioresult.Map(F.Flow3(
|
||||
exec.StdOut,
|
||||
B.ToString,
|
||||
strings.TrimSpace,
|
||||
)),
|
||||
)
|
||||
|
||||
result, err := version()
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, result)
|
||||
}
|
||||
63
v2/idiomatic/ioresult/exec/types.go
Normal file
63
v2/idiomatic/ioresult/exec/types.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// 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 exec
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
)
|
||||
|
||||
type (
|
||||
// IOResult represents an IO operation that may fail with an error.
|
||||
// It is defined as func() (T, error), following Go's idiomatic error handling pattern.
|
||||
//
|
||||
// This type is re-exported from the ioresult package for convenience when working
|
||||
// with command execution, allowing users to reference exec.IOResult instead of
|
||||
// importing the ioresult package separately.
|
||||
IOResult[T any] = ioresult.IOResult[T]
|
||||
|
||||
// Kleisli represents a function from A to IOResult[B].
|
||||
// Named after Heinrich Kleisli, it represents a monadic arrow in the IOResult monad.
|
||||
//
|
||||
// Kleisli functions are useful for chaining operations where each step may perform
|
||||
// IO and may fail. They can be composed using IOResult's Chain function.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Kleisli[A, B any] func(A) IOResult[B]
|
||||
//
|
||||
// parseConfig := func(path string) IOResult[Config] { ... }
|
||||
// validateConfig := func(cfg Config) IOResult[Config] { ... }
|
||||
//
|
||||
// // Compose Kleisli functions
|
||||
// loadAndValidate := Chain(validateConfig)(parseConfig("/config.yml"))
|
||||
Kleisli[A, B any] = ioresult.Kleisli[A, B]
|
||||
|
||||
// Operator represents a function that transforms one IOResult into another.
|
||||
// It maps IOResult[A] to IOResult[B], useful for defining reusable transformations.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Operator[A, B any] func(IOResult[A]) IOResult[B]
|
||||
//
|
||||
// addLogging := func(io IOResult[string]) IOResult[string] {
|
||||
// return func() (string, error) {
|
||||
// result, err := io()
|
||||
// log.Printf("Result: %v, Error: %v", result, err)
|
||||
// return result, err
|
||||
// }
|
||||
// }
|
||||
Operator[A, B any] = ioresult.Operator[A, B]
|
||||
)
|
||||
34
v2/idiomatic/ioresult/file/dir.go
Normal file
34
v2/idiomatic/ioresult/file/dir.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// MkdirAll create a sequence of directories, see [os.MkdirAll]
|
||||
func MkdirAll(path string, perm os.FileMode) IOResult[string] {
|
||||
return func() (string, error) {
|
||||
return path, os.MkdirAll(path, perm)
|
||||
}
|
||||
}
|
||||
|
||||
// Mkdir create a directory, see [os.Mkdir]
|
||||
func Mkdir(path string, perm os.FileMode) IOResult[string] {
|
||||
return func() (string, error) {
|
||||
return path, os.Mkdir(path, perm)
|
||||
}
|
||||
}
|
||||
128
v2/idiomatic/ioresult/file/dir_test.go
Normal file
128
v2/idiomatic/ioresult/file/dir_test.go
Normal file
@@ -0,0 +1,128 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMkdir(t *testing.T) {
|
||||
t.Run("successful mkdir", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
newDir := filepath.Join(tmpDir, "testdir")
|
||||
|
||||
result := Mkdir(newDir, 0755)
|
||||
path, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, newDir, path)
|
||||
|
||||
// Verify directory was created
|
||||
info, err := os.Stat(newDir)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, info.IsDir())
|
||||
})
|
||||
|
||||
t.Run("mkdir with existing directory", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
result := Mkdir(tmpDir, 0755)
|
||||
_, err := result()
|
||||
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("mkdir with parent directory not existing", func(t *testing.T) {
|
||||
result := Mkdir("/non/existent/parent/child", 0755)
|
||||
_, err := result()
|
||||
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMkdirAll(t *testing.T) {
|
||||
t.Run("successful mkdir all", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
nestedDir := filepath.Join(tmpDir, "level1", "level2", "level3")
|
||||
|
||||
result := MkdirAll(nestedDir, 0755)
|
||||
path, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, nestedDir, path)
|
||||
|
||||
// Verify all directories were created
|
||||
info, err := os.Stat(nestedDir)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, info.IsDir())
|
||||
|
||||
// Verify intermediate directories
|
||||
level1 := filepath.Join(tmpDir, "level1")
|
||||
info1, err := os.Stat(level1)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, info1.IsDir())
|
||||
|
||||
level2 := filepath.Join(tmpDir, "level1", "level2")
|
||||
info2, err := os.Stat(level2)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, info2.IsDir())
|
||||
})
|
||||
|
||||
t.Run("mkdirall with existing directory", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
result := MkdirAll(tmpDir, 0755)
|
||||
path, err := result()
|
||||
|
||||
// MkdirAll should succeed even if directory exists
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tmpDir, path)
|
||||
})
|
||||
|
||||
t.Run("mkdirall single level", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
newDir := filepath.Join(tmpDir, "single")
|
||||
|
||||
result := MkdirAll(newDir, 0755)
|
||||
path, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, newDir, path)
|
||||
|
||||
info, err := os.Stat(newDir)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, info.IsDir())
|
||||
})
|
||||
|
||||
t.Run("mkdirall with file in path", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
filePath := filepath.Join(tmpDir, "file.txt")
|
||||
|
||||
// Create a file
|
||||
err := os.WriteFile(filePath, []byte("content"), 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try to create a directory where file exists
|
||||
result := MkdirAll(filepath.Join(filePath, "subdir"), 0755)
|
||||
_, err = result()
|
||||
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
55
v2/idiomatic/ioresult/file/file.go
Normal file
55
v2/idiomatic/ioresult/file/file.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
)
|
||||
|
||||
var (
|
||||
// Open opens a file for reading
|
||||
Open = ioresult.Eitherize1(os.Open)
|
||||
// Create opens a file for writing
|
||||
Create = ioresult.Eitherize1(os.Create)
|
||||
// ReadFile reads the context of a file
|
||||
ReadFile = ioresult.Eitherize1(os.ReadFile)
|
||||
)
|
||||
|
||||
// WriteFile writes a data blob to a file
|
||||
func WriteFile(dstName string, perm os.FileMode) Kleisli[[]byte, []byte] {
|
||||
return func(data []byte) IOResult[[]byte] {
|
||||
return func() ([]byte, error) {
|
||||
return data, os.WriteFile(dstName, data, perm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove removes a file by name
|
||||
func Remove(name string) IOResult[string] {
|
||||
return func() (string, error) {
|
||||
return name, os.Remove(name)
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes an object
|
||||
func Close[C io.Closer](c C) IOResult[any] {
|
||||
return func() (any, error) {
|
||||
return c, c.Close()
|
||||
}
|
||||
}
|
||||
234
v2/idiomatic/ioresult/file/file_test.go
Normal file
234
v2/idiomatic/ioresult/file/file_test.go
Normal file
@@ -0,0 +1,234 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestOpen(t *testing.T) {
|
||||
t.Run("successful open", func(t *testing.T) {
|
||||
// Create a temporary file
|
||||
tmpFile, err := os.CreateTemp("", "test-open-*.txt")
|
||||
require.NoError(t, err)
|
||||
tmpPath := tmpFile.Name()
|
||||
tmpFile.Close()
|
||||
defer os.Remove(tmpPath)
|
||||
|
||||
// Write some content
|
||||
err = os.WriteFile(tmpPath, []byte("test content"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test Open
|
||||
result := Open(tmpPath)
|
||||
file, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, file)
|
||||
file.Close()
|
||||
})
|
||||
|
||||
t.Run("open non-existent file", func(t *testing.T) {
|
||||
result := Open("/path/that/does/not/exist.txt")
|
||||
_, err := result()
|
||||
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreate(t *testing.T) {
|
||||
t.Run("successful create", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testPath := filepath.Join(tmpDir, "new-file.txt")
|
||||
|
||||
result := Create(testPath)
|
||||
file, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, file)
|
||||
|
||||
// Verify file was created
|
||||
_, statErr := os.Stat(testPath)
|
||||
assert.NoError(t, statErr)
|
||||
|
||||
file.Close()
|
||||
})
|
||||
|
||||
t.Run("create in non-existent directory", func(t *testing.T) {
|
||||
result := Create("/non/existent/directory/file.txt")
|
||||
_, err := result()
|
||||
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestReadFile(t *testing.T) {
|
||||
t.Run("successful read", func(t *testing.T) {
|
||||
tmpFile, err := os.CreateTemp("", "test-read-*.txt")
|
||||
require.NoError(t, err)
|
||||
tmpPath := tmpFile.Name()
|
||||
defer os.Remove(tmpPath)
|
||||
|
||||
expectedContent := []byte("Hello, World!")
|
||||
_, err = tmpFile.Write(expectedContent)
|
||||
require.NoError(t, err)
|
||||
tmpFile.Close()
|
||||
|
||||
result := ReadFile(tmpPath)
|
||||
content, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedContent, content)
|
||||
})
|
||||
|
||||
t.Run("read non-existent file", func(t *testing.T) {
|
||||
result := ReadFile("/non/existent/file.txt")
|
||||
_, err := result()
|
||||
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("read empty file", func(t *testing.T) {
|
||||
tmpFile, err := os.CreateTemp("", "test-empty-*.txt")
|
||||
require.NoError(t, err)
|
||||
tmpPath := tmpFile.Name()
|
||||
tmpFile.Close()
|
||||
defer os.Remove(tmpPath)
|
||||
|
||||
result := ReadFile(tmpPath)
|
||||
content, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, content)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWriteFile(t *testing.T) {
|
||||
t.Run("successful write", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testPath := filepath.Join(tmpDir, "write-test.txt")
|
||||
testData := []byte("test data")
|
||||
|
||||
result := WriteFile(testPath, 0644)(testData)
|
||||
returnedData, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testData, returnedData)
|
||||
|
||||
// Verify file content
|
||||
content, err := os.ReadFile(testPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testData, content)
|
||||
})
|
||||
|
||||
t.Run("write to invalid path", func(t *testing.T) {
|
||||
testData := []byte("test data")
|
||||
result := WriteFile("/non/existent/dir/file.txt", 0644)(testData)
|
||||
_, err := result()
|
||||
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("overwrite existing file", func(t *testing.T) {
|
||||
tmpFile, err := os.CreateTemp("", "test-overwrite-*.txt")
|
||||
require.NoError(t, err)
|
||||
tmpPath := tmpFile.Name()
|
||||
tmpFile.Close()
|
||||
defer os.Remove(tmpPath)
|
||||
|
||||
// Write initial content
|
||||
err = os.WriteFile(tmpPath, []byte("initial"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Overwrite with new content
|
||||
newData := []byte("overwritten")
|
||||
result := WriteFile(tmpPath, 0644)(newData)
|
||||
returnedData, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, newData, returnedData)
|
||||
|
||||
// Verify new content
|
||||
content, err := os.ReadFile(tmpPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, newData, content)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
t.Run("successful remove", func(t *testing.T) {
|
||||
tmpFile, err := os.CreateTemp("", "test-remove-*.txt")
|
||||
require.NoError(t, err)
|
||||
tmpPath := tmpFile.Name()
|
||||
tmpFile.Close()
|
||||
|
||||
result := Remove(tmpPath)
|
||||
name, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tmpPath, name)
|
||||
|
||||
// Verify file is removed
|
||||
_, statErr := os.Stat(tmpPath)
|
||||
assert.True(t, os.IsNotExist(statErr))
|
||||
})
|
||||
|
||||
t.Run("remove non-existent file", func(t *testing.T) {
|
||||
result := Remove("/non/existent/file.txt")
|
||||
_, err := result()
|
||||
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestClose(t *testing.T) {
|
||||
t.Run("successful close", func(t *testing.T) {
|
||||
tmpFile, err := os.CreateTemp("", "test-close-*.txt")
|
||||
require.NoError(t, err)
|
||||
tmpPath := tmpFile.Name()
|
||||
defer os.Remove(tmpPath)
|
||||
|
||||
result := Close(tmpFile)
|
||||
_, err = result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify file is closed by attempting to write
|
||||
_, writeErr := tmpFile.Write([]byte("test"))
|
||||
assert.Error(t, writeErr)
|
||||
})
|
||||
|
||||
t.Run("close already closed file", func(t *testing.T) {
|
||||
tmpFile, err := os.CreateTemp("", "test-close-twice-*.txt")
|
||||
require.NoError(t, err)
|
||||
tmpPath := tmpFile.Name()
|
||||
defer os.Remove(tmpPath)
|
||||
|
||||
// Close once
|
||||
tmpFile.Close()
|
||||
|
||||
// Close again via Close function
|
||||
result := Close(tmpFile)
|
||||
_, err = result()
|
||||
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
40
v2/idiomatic/ioresult/file/readall.go
Normal file
40
v2/idiomatic/ioresult/file/readall.go
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
FL "github.com/IBM/fp-go/v2/file"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
)
|
||||
|
||||
var (
|
||||
// readAll is the adapted version of [io.ReadAll]
|
||||
readAll = ioresult.Eitherize1(io.ReadAll)
|
||||
)
|
||||
|
||||
// ReadAll uses a generator function to create a stream, reads it and closes it
|
||||
func ReadAll[R io.ReadCloser](acquire IOResult[R]) IOResult[[]byte] {
|
||||
return F.Pipe1(
|
||||
F.Flow2(
|
||||
FL.ToReader[R],
|
||||
readAll,
|
||||
),
|
||||
ioresult.WithResource[[]byte](acquire, Close[R]),
|
||||
)
|
||||
}
|
||||
137
v2/idiomatic/ioresult/file/readall_test.go
Normal file
137
v2/idiomatic/ioresult/file/readall_test.go
Normal file
@@ -0,0 +1,137 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestReadAll(t *testing.T) {
|
||||
t.Run("successful read all", func(t *testing.T) {
|
||||
|
||||
tmpFile, err := os.CreateTemp("", "test-readall-*.txt")
|
||||
require.NoError(t, err)
|
||||
tmpPath := tmpFile.Name()
|
||||
defer os.Remove(tmpPath)
|
||||
|
||||
expectedContent := []byte("Hello, ReadAll!")
|
||||
_, err = tmpFile.Write(expectedContent)
|
||||
require.NoError(t, err)
|
||||
tmpFile.Close()
|
||||
|
||||
result := ReadAll(Open(tmpPath))
|
||||
content, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedContent, content)
|
||||
})
|
||||
|
||||
t.Run("read all ensures file is closed", func(t *testing.T) {
|
||||
tmpFile, err := os.CreateTemp("", "test-readall-close-*.txt")
|
||||
require.NoError(t, err)
|
||||
tmpPath := tmpFile.Name()
|
||||
defer os.Remove(tmpPath)
|
||||
|
||||
testContent := []byte("test data for close")
|
||||
_, err = tmpFile.Write(testContent)
|
||||
require.NoError(t, err)
|
||||
tmpFile.Close()
|
||||
|
||||
var capturedFile *os.File
|
||||
acquire := func() (*os.File, error) {
|
||||
f, err := os.Open(tmpPath)
|
||||
capturedFile = f
|
||||
return f, err
|
||||
}
|
||||
|
||||
result := ReadAll(acquire)
|
||||
content, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testContent, content)
|
||||
|
||||
// Verify file is closed by trying to read
|
||||
buf := make([]byte, 10)
|
||||
_, readErr := capturedFile.Read(buf)
|
||||
assert.Error(t, readErr)
|
||||
})
|
||||
|
||||
t.Run("read all with open failure", func(t *testing.T) {
|
||||
result := ReadAll(Open("/non/existent/file.txt"))
|
||||
_, err := result()
|
||||
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("read all empty file", func(t *testing.T) {
|
||||
tmpFile, err := os.CreateTemp("", "test-readall-empty-*.txt")
|
||||
require.NoError(t, err)
|
||||
tmpPath := tmpFile.Name()
|
||||
tmpFile.Close()
|
||||
defer os.Remove(tmpPath)
|
||||
|
||||
result := ReadAll(Open(tmpPath))
|
||||
content, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, content)
|
||||
})
|
||||
|
||||
t.Run("read all large file", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testPath := filepath.Join(tmpDir, "large-file.txt")
|
||||
|
||||
// Create a larger file
|
||||
largeContent := make([]byte, 10000)
|
||||
for i := range largeContent {
|
||||
largeContent[i] = byte('A' + (i % 26))
|
||||
}
|
||||
|
||||
err := os.WriteFile(testPath, largeContent, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
result := ReadAll(Open(testPath))
|
||||
content, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, largeContent, content)
|
||||
assert.Len(t, content, 10000)
|
||||
})
|
||||
|
||||
t.Run("read all with binary data", func(t *testing.T) {
|
||||
tmpFile, err := os.CreateTemp("", "test-readall-binary-*.bin")
|
||||
require.NoError(t, err)
|
||||
tmpPath := tmpFile.Name()
|
||||
defer os.Remove(tmpPath)
|
||||
|
||||
// Write binary data
|
||||
binaryContent := []byte{0x00, 0x01, 0x02, 0xFF, 0xFE, 0xFD}
|
||||
_, err = tmpFile.Write(binaryContent)
|
||||
require.NoError(t, err)
|
||||
tmpFile.Close()
|
||||
|
||||
result := ReadAll(Open(tmpPath))
|
||||
content, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, binaryContent, content)
|
||||
})
|
||||
}
|
||||
44
v2/idiomatic/ioresult/file/tempfile.go
Normal file
44
v2/idiomatic/ioresult/file/tempfile.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 file
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
IOF "github.com/IBM/fp-go/v2/io/file"
|
||||
)
|
||||
|
||||
var (
|
||||
// CreateTemp created a temp file with proper parametrization
|
||||
CreateTemp = ioresult.Eitherize2(os.CreateTemp)
|
||||
// onCreateTempFile creates a temp file with sensible defaults
|
||||
onCreateTempFile = CreateTemp("", "*")
|
||||
// destroy handler
|
||||
onReleaseTempFile = F.Flow4(
|
||||
IOF.Close[*os.File],
|
||||
io.Map((*os.File).Name),
|
||||
ioresult.FromIO[string],
|
||||
ioresult.Chain(Remove),
|
||||
)
|
||||
)
|
||||
|
||||
// WithTempFile creates a temporary file, then invokes a callback to create a resource based on the file, then close and remove the temp file
|
||||
func WithTempFile[A any](f Kleisli[*os.File, A]) IOResult[A] {
|
||||
return ioresult.WithResource[A](onCreateTempFile, onReleaseTempFile)(f)
|
||||
}
|
||||
53
v2/idiomatic/ioresult/file/tempfile_test.go
Normal file
53
v2/idiomatic/ioresult/file/tempfile_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWithTempFile(t *testing.T) {
|
||||
|
||||
res := F.Pipe2(
|
||||
[]byte("Carsten"),
|
||||
onWriteAll[*os.File],
|
||||
WithTempFile,
|
||||
)
|
||||
|
||||
result, err := res()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("Carsten"), result)
|
||||
}
|
||||
|
||||
func TestWithTempFileOnClosedFile(t *testing.T) {
|
||||
|
||||
res := WithTempFile(func(f *os.File) IOResult[[]byte] {
|
||||
return F.Pipe2(
|
||||
f,
|
||||
onWriteAll[*os.File]([]byte("Carsten")),
|
||||
ioresult.ChainFirst(F.Constant1[[]byte](Close(f))),
|
||||
)
|
||||
})
|
||||
|
||||
result, err := res()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("Carsten"), result)
|
||||
}
|
||||
11
v2/idiomatic/ioresult/file/types.go
Normal file
11
v2/idiomatic/ioresult/file/types.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
)
|
||||
|
||||
type (
|
||||
IOResult[T any] = ioresult.IOResult[T]
|
||||
Kleisli[A, B any] = ioresult.Kleisli[A, B]
|
||||
Operator[A, B any] = ioresult.Operator[A, B]
|
||||
)
|
||||
50
v2/idiomatic/ioresult/file/write.go
Normal file
50
v2/idiomatic/ioresult/file/write.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
)
|
||||
|
||||
func onWriteAll[W io.Writer](data []byte) Kleisli[W, []byte] {
|
||||
return func(w W) IOResult[[]byte] {
|
||||
return func() ([]byte, error) {
|
||||
_, err := w.Write(data)
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WriteAll uses a generator function to create a stream, writes data to it and closes it
|
||||
func WriteAll[W io.WriteCloser](data []byte) Operator[W, []byte] {
|
||||
onWrite := onWriteAll[W](data)
|
||||
return func(onCreate IOResult[W]) IOResult[[]byte] {
|
||||
return ioresult.WithResource[[]byte](
|
||||
onCreate,
|
||||
Close[W])(
|
||||
onWrite,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Write uses a generator function to create a stream, writes data to it and closes it
|
||||
func Write[R any, W io.WriteCloser](acquire IOResult[W]) Kleisli[Kleisli[W, R], R] {
|
||||
return ioresult.WithResource[R](
|
||||
acquire,
|
||||
Close[W])
|
||||
}
|
||||
200
v2/idiomatic/ioresult/file/write_test.go
Normal file
200
v2/idiomatic/ioresult/file/write_test.go
Normal file
@@ -0,0 +1,200 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWriteAll(t *testing.T) {
|
||||
t.Run("successful write all", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testPath := filepath.Join(tmpDir, "writeall-test.txt")
|
||||
testData := []byte("Hello, WriteAll!")
|
||||
|
||||
acquire := Create(testPath)
|
||||
result := F.Pipe1(
|
||||
acquire,
|
||||
WriteAll[*os.File](testData),
|
||||
)
|
||||
|
||||
returnedData, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testData, returnedData)
|
||||
|
||||
// Verify file content
|
||||
content, err := os.ReadFile(testPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testData, content)
|
||||
})
|
||||
|
||||
t.Run("write all ensures file is closed", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testPath := filepath.Join(tmpDir, "writeall-close-test.txt")
|
||||
testData := []byte("test data")
|
||||
|
||||
var capturedFile *os.File
|
||||
acquire := func() (*os.File, error) {
|
||||
f, err := os.Create(testPath)
|
||||
capturedFile = f
|
||||
return f, err
|
||||
}
|
||||
|
||||
result := F.Pipe1(
|
||||
ioresult.FromResult(acquire()),
|
||||
WriteAll[*os.File](testData),
|
||||
)
|
||||
|
||||
_, err := result()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify file is closed by trying to write to it
|
||||
_, writeErr := capturedFile.Write([]byte("more"))
|
||||
assert.Error(t, writeErr)
|
||||
})
|
||||
|
||||
t.Run("write all with acquire failure", func(t *testing.T) {
|
||||
testData := []byte("test data")
|
||||
|
||||
acquire := Create("/non/existent/dir/file.txt")
|
||||
result := F.Pipe1(
|
||||
acquire,
|
||||
WriteAll[*os.File](testData),
|
||||
)
|
||||
|
||||
_, err := result()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("write all with empty data", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testPath := filepath.Join(tmpDir, "empty-writeall.txt")
|
||||
|
||||
acquire := Create(testPath)
|
||||
result := F.Pipe1(
|
||||
acquire,
|
||||
WriteAll[*os.File]([]byte{}),
|
||||
)
|
||||
|
||||
returnedData, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, returnedData)
|
||||
|
||||
// Verify file exists and is empty
|
||||
content, err := os.ReadFile(testPath)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, content)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWrite(t *testing.T) {
|
||||
t.Run("successful write with resource management", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testPath := filepath.Join(tmpDir, "write-test.txt")
|
||||
testData := []byte("test content")
|
||||
|
||||
acquire := Create(testPath)
|
||||
useFile := func(f *os.File) IOResult[int] {
|
||||
return func() (int, error) {
|
||||
return f.Write(testData)
|
||||
}
|
||||
}
|
||||
|
||||
result := Write[int](acquire)(useFile)
|
||||
bytesWritten, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(testData), bytesWritten)
|
||||
|
||||
// Verify file content
|
||||
content, err := os.ReadFile(testPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testData, content)
|
||||
})
|
||||
|
||||
t.Run("write ensures cleanup on success", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testPath := filepath.Join(tmpDir, "write-cleanup-test.txt")
|
||||
|
||||
var capturedFile *os.File
|
||||
acquire := func() (*os.File, error) {
|
||||
f, err := os.Create(testPath)
|
||||
capturedFile = f
|
||||
return f, err
|
||||
}
|
||||
|
||||
useFile := func(f *os.File) IOResult[string] {
|
||||
return func() (string, error) {
|
||||
_, err := f.Write([]byte("data"))
|
||||
return "success", err
|
||||
}
|
||||
}
|
||||
|
||||
result := Write[string](ioresult.FromResult(acquire()))(useFile)
|
||||
_, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify file is closed
|
||||
_, writeErr := capturedFile.Write([]byte("more"))
|
||||
assert.Error(t, writeErr)
|
||||
})
|
||||
|
||||
t.Run("write ensures cleanup on failure", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testPath := filepath.Join(tmpDir, "write-fail-test.txt")
|
||||
|
||||
var capturedFile *os.File
|
||||
acquire := func() (*os.File, error) {
|
||||
f, err := os.Create(testPath)
|
||||
capturedFile = f
|
||||
return f, err
|
||||
}
|
||||
|
||||
useFile := func(f *os.File) IOResult[string] {
|
||||
return ioresult.Left[string](assert.AnError)
|
||||
}
|
||||
|
||||
result := Write[string](ioresult.FromResult(acquire()))(useFile)
|
||||
_, err := result()
|
||||
|
||||
assert.Error(t, err)
|
||||
|
||||
// Verify file is still closed even on error
|
||||
_, writeErr := capturedFile.Write([]byte("more"))
|
||||
assert.Error(t, writeErr)
|
||||
})
|
||||
|
||||
t.Run("write with acquire failure", func(t *testing.T) {
|
||||
useFile := func(f *os.File) IOResult[string] {
|
||||
return ioresult.Of("should not run")
|
||||
}
|
||||
|
||||
result := Write[string](Create("/non/existent/dir/file.txt"))(useFile)
|
||||
_, err := result()
|
||||
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
1840
v2/idiomatic/ioresult/gen.go
Normal file
1840
v2/idiomatic/ioresult/gen.go
Normal file
File diff suppressed because it is too large
Load Diff
67
v2/idiomatic/ioresult/http/builder/builder.go
Normal file
67
v2/idiomatic/ioresult/http/builder/builder.go
Normal file
@@ -0,0 +1,67 @@
|
||||
// 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 builder
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
R "github.com/IBM/fp-go/v2/http/builder"
|
||||
H "github.com/IBM/fp-go/v2/http/headers"
|
||||
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
IOEH "github.com/IBM/fp-go/v2/idiomatic/ioresult/http"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
func Requester(builder *R.Builder) IOEH.Requester {
|
||||
|
||||
withBody := F.Curry3(func(data []byte, url string, method string) IOResult[*http.Request] {
|
||||
return func() (*http.Request, error) {
|
||||
req, err := http.NewRequest(method, url, bytes.NewReader(data))
|
||||
if err == nil {
|
||||
req.Header.Set(H.ContentLength, strconv.Itoa(len(data)))
|
||||
H.Monoid.Concat(req.Header, builder.GetHeaders())
|
||||
}
|
||||
return req, err
|
||||
}
|
||||
})
|
||||
|
||||
withoutBody := F.Curry2(func(url string, method string) IOResult[*http.Request] {
|
||||
return func() (*http.Request, error) {
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
if err == nil {
|
||||
H.Monoid.Concat(req.Header, builder.GetHeaders())
|
||||
}
|
||||
return req, err
|
||||
}
|
||||
})
|
||||
|
||||
return F.Pipe5(
|
||||
builder.GetBody(),
|
||||
option.Fold(lazy.Of(result.Of(withoutBody)), result.Map(withBody)),
|
||||
result.Ap[func(string) IOResult[*http.Request]](builder.GetTargetURL()),
|
||||
result.Flap[IOResult[*http.Request]](builder.GetMethod()),
|
||||
result.GetOrElse(ioresult.Left[*http.Request]),
|
||||
ioresult.Map(func(req *http.Request) *http.Request {
|
||||
req.Header = H.Monoid.Concat(req.Header, builder.GetHeaders())
|
||||
return req
|
||||
}),
|
||||
)
|
||||
}
|
||||
58
v2/idiomatic/ioresult/http/builder/builder_test.go
Normal file
58
v2/idiomatic/ioresult/http/builder/builder_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// 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 builder
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
R "github.com/IBM/fp-go/v2/http/builder"
|
||||
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
E "github.com/IBM/fp-go/v2/idiomatic/result"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBuilderWithQuery(t *testing.T) {
|
||||
// add some query
|
||||
withLimit := R.WithQueryArg("limit")("10")
|
||||
withURL := R.WithURL("http://www.example.org?a=b")
|
||||
|
||||
b := F.Pipe2(
|
||||
R.Default,
|
||||
withLimit,
|
||||
withURL,
|
||||
)
|
||||
|
||||
req := F.Pipe3(
|
||||
b,
|
||||
Requester,
|
||||
ioresult.Map(func(r *http.Request) *url.URL {
|
||||
return r.URL
|
||||
}),
|
||||
ioresult.ChainFirstIOK(func(u *url.URL) io.IO[any] {
|
||||
return io.FromImpure(func() {
|
||||
q := u.Query()
|
||||
assert.Equal(t, "10", q.Get("limit"))
|
||||
assert.Equal(t, "b", q.Get("a"))
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
assert.True(t, E.IsRight(req()))
|
||||
}
|
||||
22
v2/idiomatic/ioresult/http/builder/types.go
Normal file
22
v2/idiomatic/ioresult/http/builder/types.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package builder
|
||||
|
||||
import "github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
|
||||
type (
|
||||
IOResult[A any] = ioresult.IOResult[A]
|
||||
)
|
||||
137
v2/idiomatic/ioresult/http/request.go
Normal file
137
v2/idiomatic/ioresult/http/request.go
Normal file
@@ -0,0 +1,137 @@
|
||||
// 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 http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
B "github.com/IBM/fp-go/v2/bytes"
|
||||
FL "github.com/IBM/fp-go/v2/file"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
H "github.com/IBM/fp-go/v2/http"
|
||||
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
IOEF "github.com/IBM/fp-go/v2/idiomatic/ioresult/file"
|
||||
J "github.com/IBM/fp-go/v2/json"
|
||||
P "github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
type (
|
||||
client struct {
|
||||
delegate *http.Client
|
||||
doIOE Kleisli[*http.Request, *http.Response]
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
// MakeRequest is an eitherized version of [http.NewRequest]
|
||||
MakeRequest = ioresult.Eitherize3(http.NewRequest)
|
||||
makeRequest = F.Bind13of3(MakeRequest)
|
||||
|
||||
// specialize
|
||||
MakeGetRequest = makeRequest("GET", nil)
|
||||
)
|
||||
|
||||
// MakeBodyRequest creates a request that carries a body
|
||||
func MakeBodyRequest(method string, body IOResult[[]byte]) Kleisli[string, *http.Request] {
|
||||
onBody := F.Pipe1(
|
||||
body,
|
||||
ioresult.Map(F.Flow2(
|
||||
bytes.NewReader,
|
||||
FL.ToReader[*bytes.Reader],
|
||||
)),
|
||||
)
|
||||
onRelease := ioresult.Of[io.Reader]
|
||||
withMethod := F.Bind1of3(MakeRequest)(method)
|
||||
|
||||
return F.Flow2(
|
||||
F.Bind1of2(withMethod),
|
||||
ioresult.WithResource[*http.Request](onBody, onRelease),
|
||||
)
|
||||
}
|
||||
|
||||
func (client client) Do(req Requester) IOResult[*http.Response] {
|
||||
return F.Pipe1(
|
||||
req,
|
||||
ioresult.Chain(client.doIOE),
|
||||
)
|
||||
}
|
||||
|
||||
func MakeClient(httpClient *http.Client) Client {
|
||||
return client{delegate: httpClient, doIOE: ioresult.Eitherize1(httpClient.Do)}
|
||||
}
|
||||
|
||||
// ReadFullResponse sends a request, reads the response as a byte array and represents the result as a tuple
|
||||
func ReadFullResponse(client Client) Kleisli[Requester, H.FullResponse] {
|
||||
return F.Flow3(
|
||||
client.Do,
|
||||
ioresult.ChainEitherK(H.ValidateResponse),
|
||||
ioresult.Chain(func(resp *http.Response) IOResult[H.FullResponse] {
|
||||
// var x R.Reader[*http.Response, IOResult[[]byte]] = F.Flow3(
|
||||
// H.GetBody,
|
||||
// ioresult.Of,
|
||||
// IOEF.ReadAll,
|
||||
// )
|
||||
|
||||
return F.Pipe1(
|
||||
F.Pipe3(
|
||||
resp,
|
||||
H.GetBody,
|
||||
ioresult.Of,
|
||||
IOEF.ReadAll,
|
||||
),
|
||||
ioresult.Map(F.Bind1st(P.MakePair[*http.Response, []byte], resp)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// ReadAll sends a request and reads the response as bytes
|
||||
func ReadAll(client Client) Kleisli[Requester, []byte] {
|
||||
return F.Flow2(
|
||||
ReadFullResponse(client),
|
||||
ioresult.Map(H.Body),
|
||||
)
|
||||
}
|
||||
|
||||
// ReadText sends a request, reads the response and represents the response as a text string
|
||||
func ReadText(client Client) Kleisli[Requester, string] {
|
||||
return F.Flow2(
|
||||
ReadAll(client),
|
||||
ioresult.Map(B.ToString),
|
||||
)
|
||||
}
|
||||
|
||||
// readJSON sends a request, reads the response and parses the response as a []byte
|
||||
func readJSON(client Client) Kleisli[Requester, []byte] {
|
||||
return F.Flow3(
|
||||
ReadFullResponse(client),
|
||||
ioresult.ChainFirstEitherK(F.Flow2(
|
||||
H.Response,
|
||||
H.ValidateJSONResponse,
|
||||
)),
|
||||
ioresult.Map(H.Body),
|
||||
)
|
||||
}
|
||||
|
||||
// ReadJSON sends a request, reads the response and parses the response as JSON
|
||||
func ReadJSON[A any](client Client) Kleisli[Requester, A] {
|
||||
return F.Flow2(
|
||||
readJSON(client),
|
||||
ioresult.ChainEitherK(J.Unmarshal[A]),
|
||||
)
|
||||
}
|
||||
71
v2/idiomatic/ioresult/http/retry_test.go
Normal file
71
v2/idiomatic/ioresult/http/retry_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// 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 http
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
AR "github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/errors"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
E "github.com/IBM/fp-go/v2/idiomatic/result"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
R "github.com/IBM/fp-go/v2/retry"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var expLogBackoff = R.ExponentialBackoff(250 * time.Millisecond)
|
||||
|
||||
// our retry policy with a 1s cap
|
||||
var testLogPolicy = R.CapDelay(
|
||||
2*time.Second,
|
||||
R.Monoid.Concat(expLogBackoff, R.LimitRetries(20)),
|
||||
)
|
||||
|
||||
type PostItem struct {
|
||||
UserID uint `json:"userId"`
|
||||
Id uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
func TestRetryHttp(t *testing.T) {
|
||||
// URLs to try, the first URLs have an invalid hostname
|
||||
urls := AR.From("https://jsonplaceholder1.typicode.com/posts/1", "https://jsonplaceholder2.typicode.com/posts/1", "https://jsonplaceholder3.typicode.com/posts/1", "https://jsonplaceholder4.typicode.com/posts/1", "https://jsonplaceholder.typicode.com/posts/1")
|
||||
client := MakeClient(&http.Client{})
|
||||
|
||||
action := func(status R.RetryStatus) IOResult[*PostItem] {
|
||||
return F.Pipe1(
|
||||
MakeGetRequest(urls[status.IterNumber]),
|
||||
ReadJSON[*PostItem](client),
|
||||
)
|
||||
}
|
||||
|
||||
check := E.Fold(
|
||||
F.Flow2(
|
||||
errors.As[*net.DNSError](),
|
||||
O.IsSome[*net.DNSError],
|
||||
),
|
||||
F.Constant1[*PostItem](false),
|
||||
)
|
||||
|
||||
_, err := ioresult.Retrying(testLogPolicy, action, check)()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
17
v2/idiomatic/ioresult/http/types.go
Normal file
17
v2/idiomatic/ioresult/http/types.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
)
|
||||
|
||||
type (
|
||||
IOResult[A any] = ioresult.IOResult[A]
|
||||
Kleisli[A, B any] = ioresult.Kleisli[A, B]
|
||||
Requester = IOResult[*http.Request]
|
||||
|
||||
Client interface {
|
||||
Do(Requester) IOResult[*http.Response]
|
||||
}
|
||||
)
|
||||
796
v2/idiomatic/ioresult/ioeither.go
Normal file
796
v2/idiomatic/ioresult/ioeither.go
Normal file
@@ -0,0 +1,796 @@
|
||||
// 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 ioresult
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/idiomatic/result"
|
||||
"github.com/IBM/fp-go/v2/internal/chain"
|
||||
"github.com/IBM/fp-go/v2/internal/fromio"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
RES "github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
const (
|
||||
// useParallel is the feature flag to control if we use the parallel or the sequential implementation of ap
|
||||
useParallel = true
|
||||
)
|
||||
|
||||
// Left creates an IOResult that represents a failed computation with the given error.
|
||||
// When executed, it returns the zero value for type A and the provided error.
|
||||
func Left[A any](l error) IOResult[A] {
|
||||
return func() (A, error) {
|
||||
return result.Left[A](l)
|
||||
}
|
||||
}
|
||||
|
||||
// Right creates an IOResult that represents a successful computation with the given value.
|
||||
// When executed, it returns the provided value and nil error.
|
||||
func Right[A any](r A) IOResult[A] {
|
||||
return func() (A, error) {
|
||||
return result.Of(r)
|
||||
}
|
||||
}
|
||||
|
||||
// Of creates an IOResult that represents a successful computation with the given value.
|
||||
// This is an alias for Right and is the Pointed functor implementation.
|
||||
//
|
||||
//go:inline
|
||||
func Of[A any](r A) IOResult[A] {
|
||||
return Right(r)
|
||||
}
|
||||
|
||||
// MonadOf is a monadic constructor that wraps a value in an IOResult.
|
||||
// This is an alias for Of and provides the standard monad interface.
|
||||
//
|
||||
//go:inline
|
||||
func MonadOf[A any](r A) IOResult[A] {
|
||||
return Of(r)
|
||||
}
|
||||
|
||||
// LeftIO creates an IOResult from an IO computation that produces an error.
|
||||
// The error from the IO is used as the Left value.
|
||||
func LeftIO[A any](ml IO[error]) IOResult[A] {
|
||||
return func() (a A, e error) {
|
||||
e = ml()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// RightIO creates an IOResult from an IO computation that produces a value.
|
||||
// The IO is executed and its result is wrapped in a successful IOResult.
|
||||
func RightIO[A any](mr IO[A]) IOResult[A] {
|
||||
return func() (A, error) {
|
||||
return result.Of(mr())
|
||||
}
|
||||
}
|
||||
|
||||
// FromEither converts an Either (Result[A]) to an IOResult.
|
||||
// Either's Left becomes an error, Either's Right becomes a successful value.
|
||||
func FromEither[A any](e Result[A]) IOResult[A] {
|
||||
return func() (A, error) {
|
||||
return RES.Unwrap(e)
|
||||
}
|
||||
}
|
||||
|
||||
// FromResult converts a (value, error) tuple to an IOResult.
|
||||
// This is the primary way to convert Go's standard error handling pattern to IOResult.
|
||||
func FromResult[A any](a A, err error) IOResult[A] {
|
||||
return func() (A, error) {
|
||||
return a, err
|
||||
}
|
||||
}
|
||||
|
||||
// FromOption converts an Option (represented as value, bool) to an IOResult.
|
||||
// If the bool is true, the value is wrapped in a successful IOResult.
|
||||
// If the bool is false, onNone is called to generate the error.
|
||||
func FromOption[A any](onNone Lazy[error]) func(A, bool) IOResult[A] {
|
||||
return func(a A, ok bool) IOResult[A] {
|
||||
return func() (A, error) {
|
||||
if ok {
|
||||
return result.Of(a)
|
||||
}
|
||||
return result.Left[A](onNone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ChainOptionK chains a function that returns an Option (value, bool).
|
||||
// The None case (false) is converted to an error using onNone.
|
||||
func ChainOptionK[A, B any](onNone Lazy[error]) func(func(A) (B, bool)) Operator[A, B] {
|
||||
return func(f func(A) (B, bool)) Operator[A, B] {
|
||||
return func(i IOResult[A]) IOResult[B] {
|
||||
return func() (B, error) {
|
||||
a, err := i()
|
||||
if err != nil {
|
||||
return result.Left[B](err)
|
||||
}
|
||||
b, ok := f(a)
|
||||
if ok {
|
||||
return result.Of(b)
|
||||
}
|
||||
return result.Left[B](onNone())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MonadChainIOK chains an IO kleisli function to an IOResult.
|
||||
// If the IOResult fails, the function is not executed. Otherwise, the IO is executed and wrapped.
|
||||
func MonadChainIOK[A, B any](ma IOResult[A], f io.Kleisli[A, B]) IOResult[B] {
|
||||
return fromio.MonadChainIOK(
|
||||
MonadChain[A, B],
|
||||
FromIO[B],
|
||||
ma,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// ChainIOK returns an operator that chains an IO kleisli function to an IOResult.
|
||||
// The IO computation is wrapped in a successful IOResult context.
|
||||
//
|
||||
//go:inline
|
||||
func ChainIOK[A, B any](f io.Kleisli[A, B]) Operator[A, B] {
|
||||
return fromio.ChainIOK(
|
||||
Chain[A, B],
|
||||
FromIO[B],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// ChainLazyK returns an operator that chains a lazy computation to an IOResult.
|
||||
// This is an alias for ChainIOK since Lazy and IO are equivalent.
|
||||
//
|
||||
//go:inline
|
||||
func ChainLazyK[A, B any](f func(A) Lazy[B]) Operator[A, B] {
|
||||
return ChainIOK(f)
|
||||
}
|
||||
|
||||
// FromIO converts an IO computation to an IOResult that always succeeds.
|
||||
// This is an alias for RightIO.
|
||||
//
|
||||
//go:inline
|
||||
func FromIO[A any](mr IO[A]) IOResult[A] {
|
||||
return RightIO(mr)
|
||||
}
|
||||
|
||||
// FromLazy converts a lazy computation to an IOResult that always succeeds.
|
||||
// This is an alias for FromIO since Lazy and IO are equivalent.
|
||||
//
|
||||
//go:inline
|
||||
func FromLazy[A any](mr Lazy[A]) IOResult[A] {
|
||||
return FromIO(mr)
|
||||
}
|
||||
|
||||
// MonadMap transforms the value inside an IOResult using the given function.
|
||||
// If the IOResult is a Left (error), the function is not applied.
|
||||
func MonadMap[A, B any](fa IOResult[A], f func(A) B) IOResult[B] {
|
||||
return func() (B, error) {
|
||||
a, err := fa()
|
||||
if err != nil {
|
||||
return result.Left[B](err)
|
||||
}
|
||||
return result.Of(f(a))
|
||||
}
|
||||
}
|
||||
|
||||
// Map returns an operator that transforms values using the given function.
|
||||
// This is the Functor map operation for IOResult.
|
||||
func Map[A, B any](f func(A) B) Operator[A, B] {
|
||||
return function.Bind2nd(MonadMap[A, B], f)
|
||||
}
|
||||
|
||||
// MonadMapTo replaces the value in an IOResult with a constant value.
|
||||
// If the IOResult is an error, the error is preserved.
|
||||
//
|
||||
//go:inline
|
||||
func MonadMapTo[A, B any](fa IOResult[A], b B) IOResult[B] {
|
||||
return MonadMap(fa, function.Constant1[A](b))
|
||||
}
|
||||
|
||||
// MapTo returns an operator that replaces the value with a constant.
|
||||
// This is useful for discarding the result while keeping the computational context.
|
||||
//
|
||||
//go:inline
|
||||
func MapTo[A, B any](b B) Operator[A, B] {
|
||||
return function.Bind2nd(MonadMapTo[A, B], b)
|
||||
}
|
||||
|
||||
// MonadChain chains a kleisli function that depends on the current value.
|
||||
// This is the Monad bind operation for IOResult.
|
||||
func MonadChain[A, B any](fa IOResult[A], f Kleisli[A, B]) IOResult[B] {
|
||||
return func() (B, error) {
|
||||
a, err := fa()
|
||||
if err != nil {
|
||||
return result.Left[B](err)
|
||||
}
|
||||
return f(a)()
|
||||
}
|
||||
}
|
||||
|
||||
// Chain returns an operator that chains a kleisli function.
|
||||
// This enables dependent computations where the next step depends on the previous result.
|
||||
//
|
||||
//go:inline
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||
return function.Bind2nd(MonadChain[A, B], f)
|
||||
}
|
||||
|
||||
// MonadChainEitherK chains a function that returns an Either.
|
||||
// The Either is converted to IOResult: Left becomes error, Right becomes success.
|
||||
func MonadChainEitherK[A, B any](ma IOResult[A], f either.Kleisli[error, A, B]) IOResult[B] {
|
||||
return func() (B, error) {
|
||||
a, err := ma()
|
||||
if err != nil {
|
||||
return result.Left[B](err)
|
||||
}
|
||||
return either.Unwrap(f(a))
|
||||
}
|
||||
}
|
||||
|
||||
// ChainEitherK returns an operator that chains a function returning Either.
|
||||
// Either's Left becomes an error, Right becomes a successful value.
|
||||
//
|
||||
//go:inline
|
||||
func ChainEitherK[A, B any](f either.Kleisli[error, A, B]) Operator[A, B] {
|
||||
return function.Bind2nd(MonadChainEitherK[A, B], f)
|
||||
}
|
||||
|
||||
// MonadChainResultK chains a function that returns a (value, error) tuple.
|
||||
// This allows chaining standard Go functions that return errors.
|
||||
func MonadChainResultK[A, B any](ma IOResult[A], f result.Kleisli[A, B]) IOResult[B] {
|
||||
return func() (B, error) {
|
||||
a, err := ma()
|
||||
if err != nil {
|
||||
return result.Left[B](err)
|
||||
}
|
||||
return f(a)
|
||||
}
|
||||
}
|
||||
|
||||
// ChainResultK returns an operator that chains a function returning (value, error).
|
||||
// This enables integration with standard Go error handling patterns.
|
||||
//
|
||||
//go:inline
|
||||
func ChainResultK[A, B any](f result.Kleisli[A, B]) Operator[A, B] {
|
||||
return function.Bind2nd(MonadChainResultK[A, B], f)
|
||||
}
|
||||
|
||||
// MonadAp applies a function wrapped in IOResult to a value wrapped in IOResult.
|
||||
// This is the Applicative apply operation. The implementation delegates to either
|
||||
// the parallel (MonadApPar) or sequential (MonadApSeq) version based on useParallel flag.
|
||||
//
|
||||
//go:inline
|
||||
func MonadAp[B, A any](mab IOResult[func(A) B], ma IOResult[A]) IOResult[B] {
|
||||
if useParallel {
|
||||
return MonadApPar(mab, ma)
|
||||
}
|
||||
return MonadApSeq(mab, ma)
|
||||
}
|
||||
|
||||
// Ap returns an operator that applies a function in IOResult context.
|
||||
// This enables applicative-style programming with IOResult values.
|
||||
//
|
||||
//go:inline
|
||||
func Ap[B, A any](ma IOResult[A]) Operator[func(A) B, B] {
|
||||
if useParallel {
|
||||
return ApPar[B](ma)
|
||||
}
|
||||
return ApSeq[B](ma)
|
||||
}
|
||||
|
||||
// MonadApPar applies a function to a value, executing both in parallel.
|
||||
// Both IOResults are executed concurrently for better performance.
|
||||
func MonadApPar[B, A any](mab IOResult[func(A) B], ma IOResult[A]) IOResult[B] {
|
||||
return func() (B, error) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
|
||||
var fab func(A) B
|
||||
var faberr error
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
fab, faberr = mab()
|
||||
}()
|
||||
|
||||
fa, faerr := ma()
|
||||
wg.Wait()
|
||||
|
||||
if faberr != nil {
|
||||
return result.Left[B](faberr)
|
||||
}
|
||||
if faerr != nil {
|
||||
return result.Left[B](faerr)
|
||||
}
|
||||
|
||||
return result.Of(fab(fa))
|
||||
}
|
||||
}
|
||||
|
||||
// ApPar returns an operator that applies a function to a value in parallel.
|
||||
// This is the operator form of MonadApPar.
|
||||
//
|
||||
//go:inline
|
||||
func ApPar[B, A any](ma IOResult[A]) Operator[func(A) B, B] {
|
||||
return function.Bind2nd(MonadApPar[B, A], ma)
|
||||
}
|
||||
|
||||
// MonadApSeq applies a function to a value sequentially.
|
||||
// The function IOResult is executed first, then the value IOResult.
|
||||
func MonadApSeq[B, A any](mab IOResult[func(A) B], ma IOResult[A]) IOResult[B] {
|
||||
return func() (B, error) {
|
||||
fab, err := mab()
|
||||
if err != nil {
|
||||
return result.Left[B](err)
|
||||
}
|
||||
|
||||
fa, err := ma()
|
||||
if err != nil {
|
||||
return result.Left[B](err)
|
||||
}
|
||||
|
||||
return result.Of(fab(fa))
|
||||
}
|
||||
}
|
||||
|
||||
// ApSeq returns an operator that applies a function to a value sequentially.
|
||||
// This is the operator form of MonadApSeq.
|
||||
//
|
||||
//go:inline
|
||||
func ApSeq[B, A any](ma IOResult[A]) func(IOResult[func(A) B]) IOResult[B] {
|
||||
return function.Bind2nd(MonadApSeq[B, A], ma)
|
||||
}
|
||||
|
||||
// Flatten removes one level of nesting from a nested IOResult.
|
||||
// This is equivalent to joining or flattening the structure: IOResult[IOResult[A]] -> IOResult[A].
|
||||
//
|
||||
//go:inline
|
||||
func Flatten[A any](mma IOResult[IOResult[A]]) IOResult[A] {
|
||||
return MonadChain(mma, function.Identity[IOResult[A]])
|
||||
}
|
||||
|
||||
// Memoize caches the result of an IOResult so it only executes once.
|
||||
// Subsequent calls return the cached result without re-executing the computation.
|
||||
func Memoize[A any](ma IOResult[A]) IOResult[A] {
|
||||
// synchronization primitives
|
||||
var once sync.Once
|
||||
var fa A
|
||||
var faerr error
|
||||
// callback
|
||||
gen := func() {
|
||||
fa, faerr = ma()
|
||||
}
|
||||
// returns our memoized wrapper
|
||||
return func() (A, error) {
|
||||
once.Do(gen)
|
||||
return fa, faerr
|
||||
}
|
||||
}
|
||||
|
||||
// MonadMapLeft transforms the error value using the given function.
|
||||
// The success value is left unchanged.
|
||||
func MonadMapLeft[A any](fa IOResult[A], f Endomorphism[error]) IOResult[A] {
|
||||
return func() (A, error) {
|
||||
a, err := fa()
|
||||
if err != nil {
|
||||
return result.Left[A](f(err))
|
||||
}
|
||||
return result.Of(a)
|
||||
}
|
||||
}
|
||||
|
||||
// MapLeft returns an operator that transforms the error using the given function.
|
||||
// This is useful for error wrapping or enrichment.
|
||||
//
|
||||
//go:inline
|
||||
func MapLeft[A any](f Endomorphism[error]) Operator[A, A] {
|
||||
return function.Bind2nd(MonadMapLeft[A], f)
|
||||
}
|
||||
|
||||
// MonadBiMap transforms both the error (left) and success (right) values.
|
||||
func MonadBiMap[A, B any](fa IOResult[A], f Endomorphism[error], g func(A) B) IOResult[B] {
|
||||
return func() (B, error) {
|
||||
a, err := fa()
|
||||
if err != nil {
|
||||
return result.Left[B](f(err))
|
||||
}
|
||||
return result.Of(g(a))
|
||||
}
|
||||
}
|
||||
|
||||
// BiMap returns an operator that transforms both error and success values.
|
||||
// This is the Bifunctor map operation for IOResult.
|
||||
//
|
||||
//go:inline
|
||||
func BiMap[A, B any](f Endomorphism[error], g func(A) B) Operator[A, B] {
|
||||
return function.Bind23of3(MonadBiMap[A, B])(f, g)
|
||||
}
|
||||
|
||||
// Fold returns a function that handles both error and success cases, converting to IO.
|
||||
// This is the operator form of MonadFold for pattern matching on IOResult.
|
||||
//
|
||||
//go:inline
|
||||
func Fold[A, B any](onLeft func(error) IO[B], onRight io.Kleisli[A, B]) func(IOResult[A]) IO[B] {
|
||||
return function.Bind23of3(MonadFold[A, B])(onLeft, onRight)
|
||||
}
|
||||
|
||||
// GetOrElse extracts the value from an IOResult, using a default IO for error cases.
|
||||
// This converts an IOResult to an IO that cannot fail.
|
||||
func GetOrElse[A any](onLeft func(error) IO[A]) func(IOResult[A]) IO[A] {
|
||||
return func(fa IOResult[A]) IO[A] {
|
||||
return func() A {
|
||||
a, err := fa()
|
||||
if err != nil {
|
||||
return onLeft(err)()
|
||||
}
|
||||
return a
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MonadChainTo chains two IOResults, discarding the first value if successful.
|
||||
// This is useful for sequencing computations where only the second result matters.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainTo[A, B any](fa IOResult[A], fb IOResult[B]) IOResult[B] {
|
||||
return MonadChain(fa, function.Constant1[A](fb))
|
||||
}
|
||||
|
||||
// ChainTo returns an operator that sequences two computations, keeping only the second result.
|
||||
// This is the operator form of MonadChainTo.
|
||||
//
|
||||
//go:inline
|
||||
func ChainTo[A, B any](fb IOResult[B]) Operator[A, B] {
|
||||
return function.Bind2nd(MonadChainTo[A, B], fb)
|
||||
}
|
||||
|
||||
// MonadChainFirst chains a computation but returns the original value if both succeed.
|
||||
// If either computation fails, the error is returned.
|
||||
func MonadChainFirst[A, B any](ma IOResult[A], f Kleisli[A, B]) IOResult[A] {
|
||||
return chain.MonadChainFirst(
|
||||
MonadChain[A, A],
|
||||
MonadMap[B, A],
|
||||
ma,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadTap executes a side effect but returns the original value.
|
||||
// This is an alias for MonadChainFirst, useful for logging or side effects.
|
||||
//
|
||||
//go:inline
|
||||
func MonadTap[A, B any](ma IOResult[A], f Kleisli[A, B]) IOResult[A] {
|
||||
return MonadChainFirst(ma, f)
|
||||
}
|
||||
|
||||
// ChainFirst returns an operator that runs a computation for side effects but keeps the original value.
|
||||
// This is useful for operations like validation or logging.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
||||
return chain.ChainFirst(
|
||||
Chain[A, A],
|
||||
Map[B, A],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// Tap returns an operator that executes side effects while preserving the original value.
|
||||
// This is an alias for ChainFirst.
|
||||
//
|
||||
//go:inline
|
||||
func Tap[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
||||
return ChainFirst(f)
|
||||
}
|
||||
|
||||
// MonadChainFirstEitherK runs an Either computation for side effects but returns the original value.
|
||||
// The Either computation must succeed for the original value to be returned.
|
||||
func MonadChainFirstEitherK[A, B any](ma IOResult[A], f either.Kleisli[error, A, B]) IOResult[A] {
|
||||
return func() (A, error) {
|
||||
a, err := ma()
|
||||
if err != nil {
|
||||
return result.Left[A](err)
|
||||
}
|
||||
_, err = either.Unwrap(f(a))
|
||||
if err != nil {
|
||||
return result.Left[A](err)
|
||||
}
|
||||
return result.Of(a)
|
||||
}
|
||||
}
|
||||
|
||||
// ChainFirstEitherK returns an operator that runs an Either computation for side effects.
|
||||
// This is useful for validation that may fail.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstEitherK[A, B any](f either.Kleisli[error, A, B]) Operator[A, A] {
|
||||
return function.Bind2nd(MonadChainFirstEitherK[A, B], f)
|
||||
}
|
||||
|
||||
// MonadChainFirstResultK runs a Result computation for side effects but returns the original value.
|
||||
// The Result computation must succeed for the original value to be returned.
|
||||
func MonadChainFirstResultK[A, B any](ma IOResult[A], f result.Kleisli[A, B]) IOResult[A] {
|
||||
return func() (A, error) {
|
||||
a, err := ma()
|
||||
if err != nil {
|
||||
return result.Left[A](err)
|
||||
}
|
||||
_, err = f(a)
|
||||
if err != nil {
|
||||
return result.Left[A](err)
|
||||
}
|
||||
return result.Of(a)
|
||||
}
|
||||
}
|
||||
|
||||
// ChainFirstResultK returns an operator that runs a Result computation for side effects.
|
||||
// This integrates with standard Go error-returning functions.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstResultK[A, B any](f result.Kleisli[A, B]) Operator[A, A] {
|
||||
return function.Bind2nd(MonadChainFirstResultK[A, B], f)
|
||||
}
|
||||
|
||||
// MonadChainFirstIOK runs an IO computation for side effects but returns the original value.
|
||||
// The IO computation always succeeds, so errors from the original IOResult are preserved.
|
||||
func MonadChainFirstIOK[A, B any](ma IOResult[A], f io.Kleisli[A, B]) IOResult[A] {
|
||||
return fromio.MonadChainFirstIOK(
|
||||
MonadChain[A, A],
|
||||
MonadMap[B, A],
|
||||
FromIO[B],
|
||||
ma,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// ChainFirstIOK returns an operator that runs an IO computation for side effects.
|
||||
// This is useful for operations like logging that cannot fail.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstIOK[A, B any](f io.Kleisli[A, B]) Operator[A, A] {
|
||||
return fromio.ChainFirstIOK(
|
||||
Chain[A, A],
|
||||
Map[B, A],
|
||||
FromIO[B],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadTapEitherK executes an Either computation for side effects.
|
||||
// This is an alias for MonadChainFirstEitherK.
|
||||
//
|
||||
//go:inline
|
||||
func MonadTapEitherK[A, B any](ma IOResult[A], f either.Kleisli[error, A, B]) IOResult[A] {
|
||||
return MonadChainFirstEitherK(ma, f)
|
||||
}
|
||||
|
||||
// TapEitherK returns an operator that executes an Either computation for side effects.
|
||||
// This is an alias for ChainFirstEitherK.
|
||||
//
|
||||
//go:inline
|
||||
func TapEitherK[A, B any](f either.Kleisli[error, A, B]) Operator[A, A] {
|
||||
return ChainFirstEitherK(f)
|
||||
}
|
||||
|
||||
// MonadTapResultK executes a Result computation for side effects.
|
||||
// This is an alias for MonadChainFirstResultK.
|
||||
//
|
||||
//go:inline
|
||||
func MonadTapResultK[A, B any](ma IOResult[A], f result.Kleisli[A, B]) IOResult[A] {
|
||||
return MonadChainFirstResultK(ma, f)
|
||||
}
|
||||
|
||||
// TapResultK returns an operator that executes a Result computation for side effects.
|
||||
// This is an alias for ChainFirstResultK.
|
||||
//
|
||||
//go:inline
|
||||
func TapResultK[A, B any](f result.Kleisli[A, B]) Operator[A, A] {
|
||||
return ChainFirstResultK(f)
|
||||
}
|
||||
|
||||
// MonadTapIOK executes an IO computation for side effects.
|
||||
// This is an alias for MonadChainFirstIOK.
|
||||
//
|
||||
//go:inline
|
||||
func MonadTapIOK[A, B any](ma IOResult[A], f io.Kleisli[A, B]) IOResult[A] {
|
||||
return MonadChainFirstIOK(ma, f)
|
||||
}
|
||||
|
||||
// TapIOK returns an operator that executes an IO computation for side effects.
|
||||
// This is an alias for ChainFirstIOK.
|
||||
//
|
||||
//go:inline
|
||||
func TapIOK[A, B any](f io.Kleisli[A, B]) Operator[A, A] {
|
||||
return ChainFirstIOK(f)
|
||||
}
|
||||
|
||||
// MonadFold handles both error and success cases explicitly, converting to an IO.
|
||||
// This is useful for pattern matching on the IOResult.
|
||||
func MonadFold[A, B any](ma IOResult[A], onLeft func(error) IO[B], onRight io.Kleisli[A, B]) IO[B] {
|
||||
return func() B {
|
||||
a, err := ma()
|
||||
if err != nil {
|
||||
return onLeft(err)()
|
||||
}
|
||||
return onRight(a)()
|
||||
}
|
||||
}
|
||||
|
||||
// WithResource constructs a function that creates a resource, then operates on it and then releases the resource
|
||||
// WithResource constructs a bracket pattern for resource management.
|
||||
// It ensures resources are properly acquired, used, and released even if errors occur.
|
||||
// The release function is always called, similar to defer.
|
||||
func WithResource[A, R, ANY any](
|
||||
onCreate IOResult[R],
|
||||
onRelease Kleisli[R, ANY],
|
||||
) Kleisli[Kleisli[R, A], A] {
|
||||
return func(k Kleisli[R, A]) IOResult[A] {
|
||||
return func() (A, error) {
|
||||
r, rerr := onCreate()
|
||||
if rerr != nil {
|
||||
return result.Left[A](rerr)
|
||||
}
|
||||
a, aerr := k(r)()
|
||||
_, nerr := onRelease(r)()
|
||||
if aerr != nil {
|
||||
return result.Left[A](aerr)
|
||||
}
|
||||
if nerr != nil {
|
||||
return result.Left[A](nerr)
|
||||
}
|
||||
return result.Of(a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FromImpure converts an impure side-effecting function into an IOResult.
|
||||
// The function is executed when the IOResult runs, and always succeeds with nil.
|
||||
func FromImpure(f func()) IOResult[any] {
|
||||
return function.Pipe2(
|
||||
f,
|
||||
io.FromImpure,
|
||||
FromIO[any],
|
||||
)
|
||||
}
|
||||
|
||||
// Defer defers the creation of an IOResult until it is executed.
|
||||
// This allows lazy evaluation of the IOResult itself.
|
||||
func Defer[A any](gen Lazy[IOResult[A]]) IOResult[A] {
|
||||
return func() (A, error) {
|
||||
return gen()()
|
||||
}
|
||||
}
|
||||
|
||||
// MonadAlt tries the first IOResult, and if it fails, tries the second.
|
||||
// This provides a fallback mechanism for error recovery.
|
||||
func MonadAlt[A any](first IOResult[A], second Lazy[IOResult[A]]) IOResult[A] {
|
||||
return func() (A, error) {
|
||||
a, err := first()
|
||||
if err != nil {
|
||||
return second()()
|
||||
}
|
||||
return result.Of(a)
|
||||
}
|
||||
}
|
||||
|
||||
// Alt returns an operator that provides a fallback for error recovery.
|
||||
// This identifies an associative operation on a type constructor for the Alternative instance.
|
||||
func Alt[A any](second Lazy[IOResult[A]]) Operator[A, A] {
|
||||
return function.Bind2nd(MonadAlt[A], second)
|
||||
}
|
||||
|
||||
// MonadFlap applies a fixed argument to a function inside an IOResult.
|
||||
// This is the reverse of Ap: the function is wrapped and the argument is fixed.
|
||||
//
|
||||
//go:inline
|
||||
func MonadFlap[B, A any](fab IOResult[func(A) B], a A) IOResult[B] {
|
||||
return functor.MonadFlap(MonadMap[func(A) B, B], fab, a)
|
||||
}
|
||||
|
||||
// Flap returns an operator that applies a fixed argument to a wrapped function.
|
||||
// This is useful for partial application with wrapped functions.
|
||||
//
|
||||
//go:inline
|
||||
func Flap[B, A any](a A) Operator[func(A) B, B] {
|
||||
return functor.Flap(Map[func(A) B, B], a)
|
||||
}
|
||||
|
||||
// Delay creates an operator that delays execution by the specified duration.
|
||||
// The IOResult is executed after waiting for the given duration.
|
||||
func Delay[A any](delay time.Duration) Operator[A, A] {
|
||||
return func(fa IOResult[A]) IOResult[A] {
|
||||
return func() (A, error) {
|
||||
time.Sleep(delay)
|
||||
return fa()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// After creates an operator that delays execution until the specified timestamp.
|
||||
// If the timestamp is in the past, the IOResult executes immediately.
|
||||
func After[A any](timestamp time.Time) Operator[A, A] {
|
||||
return func(fa IOResult[A]) IOResult[A] {
|
||||
return func() (A, error) {
|
||||
// check if we need to wait
|
||||
current := time.Now()
|
||||
if current.Before(timestamp) {
|
||||
time.Sleep(timestamp.Sub(current))
|
||||
}
|
||||
return fa()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MonadChainLeft handles the error case by chaining to a new computation.
|
||||
// If the IOResult succeeds, it passes through unchanged.
|
||||
func MonadChainLeft[A any](fa IOResult[A], f Kleisli[error, A]) IOResult[A] {
|
||||
return func() (A, error) {
|
||||
a, err := fa()
|
||||
if err != nil {
|
||||
return f(err)()
|
||||
}
|
||||
return result.Of(a)
|
||||
}
|
||||
}
|
||||
|
||||
// ChainLeft returns an operator that handles errors with a recovery computation.
|
||||
// This enables error recovery and transformation.
|
||||
//
|
||||
//go:inline
|
||||
func ChainLeft[A any](f Kleisli[error, A]) Operator[A, A] {
|
||||
return function.Bind2nd(MonadChainLeft[A], f)
|
||||
}
|
||||
|
||||
// MonadChainFirstLeft runs a computation on the error but always returns the original error.
|
||||
// This is useful for side effects like logging errors without recovery.
|
||||
func MonadChainFirstLeft[A, B any](ma IOResult[A], f Kleisli[error, B]) IOResult[A] {
|
||||
return func() (A, error) {
|
||||
a, err := ma()
|
||||
if err != nil {
|
||||
_, _ = f(err)()
|
||||
return result.Left[A](err)
|
||||
}
|
||||
return result.Of(a)
|
||||
}
|
||||
}
|
||||
|
||||
// MonadTapLeft executes a side effect on errors without changing the error.
|
||||
// This is an alias for MonadChainFirstLeft, useful for error logging.
|
||||
//
|
||||
//go:inline
|
||||
func MonadTapLeft[A, B any](ma IOResult[A], f Kleisli[error, B]) IOResult[A] {
|
||||
return MonadChainFirstLeft(ma, f)
|
||||
}
|
||||
|
||||
// ChainFirstLeft returns an operator that runs a computation on errors for side effects.
|
||||
// The original error is always preserved.
|
||||
func ChainFirstLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
|
||||
return function.Bind2nd(MonadChainFirstLeft[A, B], f)
|
||||
}
|
||||
|
||||
// TapLeft returns an operator that executes side effects on errors.
|
||||
// This is an alias for ChainFirstLeft.
|
||||
//
|
||||
//go:inline
|
||||
func TapLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
|
||||
return ChainFirstLeft[A](f)
|
||||
}
|
||||
452
v2/idiomatic/ioresult/ioeither_test.go
Normal file
452
v2/idiomatic/ioresult/ioeither_test.go
Normal file
@@ -0,0 +1,452 @@
|
||||
// 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 ioresult
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
result, err := F.Pipe1(
|
||||
Of(1),
|
||||
Map(utils.Double),
|
||||
)()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, result)
|
||||
|
||||
}
|
||||
|
||||
func TestChainEitherK(t *testing.T) {
|
||||
f := ChainResultK(func(n int) (int, error) {
|
||||
if n > 0 {
|
||||
return n, nil
|
||||
}
|
||||
return 0, errors.New("a")
|
||||
})
|
||||
result1, err1 := f(Right(1))()
|
||||
assert.NoError(t, err1)
|
||||
assert.Equal(t, 1, result1)
|
||||
|
||||
_, err2 := f(Right(-1))()
|
||||
assert.Error(t, err2)
|
||||
assert.Equal(t, "a", err2.Error())
|
||||
|
||||
_, err3 := f(Left[int](fmt.Errorf("b")))()
|
||||
assert.Error(t, err3)
|
||||
assert.Equal(t, "b", err3.Error())
|
||||
}
|
||||
|
||||
func TestChainOptionK(t *testing.T) {
|
||||
f := ChainOptionK[int, int](func() error { return fmt.Errorf("a") })(func(n int) (int, bool) {
|
||||
if n > 0 {
|
||||
return n, true
|
||||
}
|
||||
return 0, false
|
||||
})
|
||||
|
||||
result1, err1 := f(Right(1))()
|
||||
assert.NoError(t, err1)
|
||||
assert.Equal(t, 1, result1)
|
||||
|
||||
_, err2 := f(Right(-1))()
|
||||
assert.Error(t, err2)
|
||||
assert.Equal(t, "a", err2.Error())
|
||||
|
||||
_, err3 := f(Left[int](fmt.Errorf("b")))()
|
||||
assert.Error(t, err3)
|
||||
assert.Equal(t, "b", err3.Error())
|
||||
}
|
||||
|
||||
func TestFromOption(t *testing.T) {
|
||||
f := FromOption[int](func() error { return errors.New("a") })
|
||||
|
||||
result1, err1 := f(1, true)()
|
||||
assert.NoError(t, err1)
|
||||
assert.Equal(t, 1, result1)
|
||||
|
||||
_, err2 := f(0, false)()
|
||||
assert.Error(t, err2)
|
||||
assert.Equal(t, "a", err2.Error())
|
||||
}
|
||||
|
||||
func TestChainIOK(t *testing.T) {
|
||||
f := ChainIOK(func(n int) IO[string] {
|
||||
return func() string {
|
||||
return fmt.Sprintf("%d", n)
|
||||
}
|
||||
})
|
||||
|
||||
result1, err1 := f(Right(1))()
|
||||
assert.NoError(t, err1)
|
||||
assert.Equal(t, "1", result1)
|
||||
|
||||
_, err2 := f(Left[int](errors.New("b")))()
|
||||
assert.Error(t, err2)
|
||||
assert.Equal(t, "b", err2.Error())
|
||||
}
|
||||
|
||||
func TestChainWithIO(t *testing.T) {
|
||||
|
||||
r := F.Pipe1(
|
||||
Of("test"),
|
||||
ChainIOK(func(s string) IO[bool] {
|
||||
return func() bool {
|
||||
return len(s) > 0
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
result, err := r()
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, result)
|
||||
}
|
||||
|
||||
func TestChainFirst(t *testing.T) {
|
||||
f := func(a string) IOResult[int] {
|
||||
if len(a) > 2 {
|
||||
return Of(len(a))
|
||||
}
|
||||
return Left[int](errors.New("foo"))
|
||||
}
|
||||
good := Of("foo")
|
||||
bad := Of("a")
|
||||
ch := ChainFirst(f)
|
||||
|
||||
result1, err1 := F.Pipe1(good, ch)()
|
||||
assert.NoError(t, err1)
|
||||
assert.Equal(t, "foo", result1)
|
||||
|
||||
_, err2 := F.Pipe1(bad, ch)()
|
||||
assert.Error(t, err2)
|
||||
assert.Equal(t, "foo", err2.Error())
|
||||
}
|
||||
|
||||
func TestChainFirstIOK(t *testing.T) {
|
||||
f := func(a string) IO[int] {
|
||||
return io.Of(len(a))
|
||||
}
|
||||
good := Of("foo")
|
||||
ch := ChainFirstIOK(f)
|
||||
|
||||
result, err := F.Pipe1(good, ch)()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "foo", result)
|
||||
}
|
||||
|
||||
func TestMonadChainLeft(t *testing.T) {
|
||||
// Test with Left value - should apply the function
|
||||
t.Run("Left value applies function", func(t *testing.T) {
|
||||
result := MonadChainLeft(
|
||||
Left[int](errors.New("error1")),
|
||||
func(e error) IOResult[int] {
|
||||
return Left[int](errors.New("transformed: " + e.Error()))
|
||||
},
|
||||
)
|
||||
_, err := result()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "transformed: error1", err.Error())
|
||||
})
|
||||
|
||||
// Test with Left value - function returns Right (error recovery)
|
||||
t.Run("Left value recovers to Right", func(t *testing.T) {
|
||||
result := MonadChainLeft(
|
||||
Left[int](errors.New("recoverable")),
|
||||
func(e error) IOResult[int] {
|
||||
if e.Error() == "recoverable" {
|
||||
return Right(42)
|
||||
}
|
||||
return Left[int](e)
|
||||
},
|
||||
)
|
||||
val, err := result()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, val)
|
||||
})
|
||||
|
||||
// Test with Right value - should pass through unchanged
|
||||
t.Run("Right value passes through", func(t *testing.T) {
|
||||
result := MonadChainLeft(
|
||||
Right(100),
|
||||
func(e error) IOResult[int] {
|
||||
return Left[int](errors.New("should not be called"))
|
||||
},
|
||||
)
|
||||
val, err := result()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 100, val)
|
||||
})
|
||||
|
||||
// Test error type transformation
|
||||
t.Run("Error type transformation", func(t *testing.T) {
|
||||
result := MonadChainLeft(
|
||||
Left[int](errors.New("404")),
|
||||
func(e error) IOResult[int] {
|
||||
return Left[int](errors.New("404"))
|
||||
},
|
||||
)
|
||||
_, err := result()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "404", err.Error())
|
||||
})
|
||||
}
|
||||
|
||||
func TestChainLeft(t *testing.T) {
|
||||
// Test with Left value - should apply the function
|
||||
t.Run("Left value applies function", func(t *testing.T) {
|
||||
chainFn := ChainLeft(func(e error) IOResult[int] {
|
||||
return Left[int](errors.New("chained: " + e.Error()))
|
||||
})
|
||||
result := F.Pipe1(
|
||||
Left[int](errors.New("original")),
|
||||
chainFn,
|
||||
)
|
||||
_, err := result()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "chained: original", err.Error())
|
||||
})
|
||||
|
||||
// Test with Left value - function returns Right (error recovery)
|
||||
t.Run("Left value recovers to Right", func(t *testing.T) {
|
||||
chainFn := ChainLeft(func(e error) IOResult[int] {
|
||||
if e.Error() == "network error" {
|
||||
return Right(0) // default value
|
||||
}
|
||||
return Left[int](e)
|
||||
})
|
||||
result := F.Pipe1(
|
||||
Left[int](errors.New("network error")),
|
||||
chainFn,
|
||||
)
|
||||
val, err := result()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, val)
|
||||
})
|
||||
|
||||
// Test with Right value - should pass through unchanged
|
||||
t.Run("Right value passes through", func(t *testing.T) {
|
||||
chainFn := ChainLeft(func(e error) IOResult[int] {
|
||||
return Left[int](errors.New("should not be called"))
|
||||
})
|
||||
result := F.Pipe1(
|
||||
Right(42),
|
||||
chainFn,
|
||||
)
|
||||
val, err := result()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, val)
|
||||
})
|
||||
|
||||
// Test composition with other operations
|
||||
t.Run("Composition with Map", func(t *testing.T) {
|
||||
result := F.Pipe2(
|
||||
Left[int](errors.New("error")),
|
||||
ChainLeft(func(e error) IOResult[int] {
|
||||
return Left[int](errors.New("handled: " + e.Error()))
|
||||
}),
|
||||
Map(utils.Double),
|
||||
)
|
||||
_, err := result()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "handled: error", err.Error())
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadChainFirstLeft(t *testing.T) {
|
||||
// Test with Left value - function returns Left, always preserves original error
|
||||
t.Run("Left value with function returning Left preserves original error", func(t *testing.T) {
|
||||
sideEffectCalled := false
|
||||
result := MonadChainFirstLeft(
|
||||
Left[int](errors.New("original error")),
|
||||
func(e error) IOResult[int] {
|
||||
sideEffectCalled = true
|
||||
return Left[int](errors.New("new error")) // This error is ignored, original is returned
|
||||
},
|
||||
)
|
||||
_, err := result()
|
||||
assert.True(t, sideEffectCalled)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "original error", err.Error())
|
||||
})
|
||||
|
||||
// Test with Left value - function returns Right, still returns original Left
|
||||
t.Run("Left value with function returning Right still returns original Left", func(t *testing.T) {
|
||||
var capturedError string
|
||||
result := MonadChainFirstLeft(
|
||||
Left[int](errors.New("validation failed")),
|
||||
func(e error) IOResult[int] {
|
||||
capturedError = e.Error()
|
||||
return Right(999) // This Right value is ignored, original Left is returned
|
||||
},
|
||||
)
|
||||
_, err := result()
|
||||
assert.Equal(t, "validation failed", capturedError)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "validation failed", err.Error())
|
||||
})
|
||||
|
||||
// Test with Right value - should pass through without calling function
|
||||
t.Run("Right value passes through", func(t *testing.T) {
|
||||
sideEffectCalled := false
|
||||
result := MonadChainFirstLeft(
|
||||
Right(42),
|
||||
func(e error) IOResult[int] {
|
||||
sideEffectCalled = true
|
||||
return Left[int](errors.New("should not be called"))
|
||||
},
|
||||
)
|
||||
assert.False(t, sideEffectCalled)
|
||||
val, err := result()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, val)
|
||||
})
|
||||
|
||||
// Test that side effects are executed but original error is always preserved
|
||||
t.Run("Side effects executed but original error preserved", func(t *testing.T) {
|
||||
effectCount := 0
|
||||
result := MonadChainFirstLeft(
|
||||
Left[int](errors.New("original error")),
|
||||
func(e error) IOResult[int] {
|
||||
effectCount++
|
||||
// Try to return Right, but original Left should still be returned
|
||||
return Right(999)
|
||||
},
|
||||
)
|
||||
_, err := result()
|
||||
assert.Equal(t, 1, effectCount)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "original error", err.Error())
|
||||
})
|
||||
}
|
||||
|
||||
func TestChainFirstLeft(t *testing.T) {
|
||||
// Test with Left value - function returns Left, always preserves original error
|
||||
t.Run("Left value with function returning Left preserves error", func(t *testing.T) {
|
||||
var captured string
|
||||
chainFn := ChainFirstLeft[int](func(e error) IOResult[int] {
|
||||
captured = e.Error()
|
||||
return Left[int](errors.New("ignored error"))
|
||||
})
|
||||
result := F.Pipe1(
|
||||
Left[int](errors.New("test error")),
|
||||
chainFn,
|
||||
)
|
||||
_, err := result()
|
||||
assert.Equal(t, "test error", captured)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "test error", err.Error())
|
||||
})
|
||||
|
||||
// Test with Left value - function returns Right, still returns original Left
|
||||
t.Run("Left value with function returning Right still returns original Left", func(t *testing.T) {
|
||||
var captured string
|
||||
chainFn := ChainFirstLeft[int](func(e error) IOResult[int] {
|
||||
captured = e.Error()
|
||||
return Right(42) // This Right is ignored, original Left is returned
|
||||
})
|
||||
result := F.Pipe1(
|
||||
Left[int](errors.New("test error")),
|
||||
chainFn,
|
||||
)
|
||||
_, err := result()
|
||||
assert.Equal(t, "test error", captured)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "test error", err.Error())
|
||||
})
|
||||
|
||||
// Test with Right value - should pass through without calling function
|
||||
t.Run("Right value passes through", func(t *testing.T) {
|
||||
called := false
|
||||
chainFn := ChainFirstLeft[int](func(e error) IOResult[int] {
|
||||
called = true
|
||||
return Right(0)
|
||||
})
|
||||
result := F.Pipe1(
|
||||
Right(100),
|
||||
chainFn,
|
||||
)
|
||||
assert.False(t, called)
|
||||
val, err := result()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 100, val)
|
||||
})
|
||||
|
||||
// Test that original error is always preserved regardless of what f returns
|
||||
t.Run("Original error always preserved", func(t *testing.T) {
|
||||
chainFn := ChainFirstLeft[int](func(e error) IOResult[int] {
|
||||
// Try to return Right, but original Left should still be returned
|
||||
return Right(999)
|
||||
})
|
||||
|
||||
result := F.Pipe1(
|
||||
Left[int](errors.New("original")),
|
||||
chainFn,
|
||||
)
|
||||
_, err := result()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "original", err.Error())
|
||||
})
|
||||
|
||||
// Test with IO side effects - original Left is always preserved
|
||||
t.Run("IO side effects with Left preservation", func(t *testing.T) {
|
||||
effectCount := 0
|
||||
chainFn := ChainFirstLeft[int](func(e error) IOResult[int] {
|
||||
return FromIO(func() int {
|
||||
effectCount++
|
||||
return 0
|
||||
})
|
||||
})
|
||||
|
||||
// Even though FromIO wraps in Right, the original Left is preserved
|
||||
result := F.Pipe1(
|
||||
Left[int](errors.New("error")),
|
||||
chainFn,
|
||||
)
|
||||
|
||||
_, err := result()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "error", err.Error())
|
||||
assert.Equal(t, 1, effectCount)
|
||||
})
|
||||
|
||||
// Test logging with Left preservation
|
||||
t.Run("Logging with Left preservation", func(t *testing.T) {
|
||||
errorLog := []string{}
|
||||
logError := ChainFirstLeft[string](func(e error) IOResult[string] {
|
||||
errorLog = append(errorLog, "Logged: "+e.Error())
|
||||
return Left[string](errors.New("log entry")) // This is ignored, original is preserved
|
||||
})
|
||||
|
||||
result := F.Pipe2(
|
||||
Left[string](errors.New("step1")),
|
||||
logError,
|
||||
ChainLeft(func(e error) IOResult[string] {
|
||||
return Left[string](errors.New("step2"))
|
||||
}),
|
||||
)
|
||||
|
||||
_, err := result()
|
||||
assert.Equal(t, []string{"Logged: step1"}, errorLog)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "step2", err.Error())
|
||||
})
|
||||
}
|
||||
40
v2/idiomatic/ioresult/logging.go
Normal file
40
v2/idiomatic/ioresult/logging.go
Normal file
@@ -0,0 +1,40 @@
|
||||
// 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 ioresult
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
|
||||
"github.com/IBM/fp-go/v2/idiomatic/result"
|
||||
)
|
||||
|
||||
// LogJSON converts the argument to pretty printed JSON and then logs it via the format string
|
||||
// Can be used with [ChainFirst]
|
||||
func LogJSON[A any](prefix string) Kleisli[A, any] {
|
||||
return func(a A) IOResult[any] {
|
||||
// convert to a string
|
||||
b, jsonerr := json.MarshalIndent(a, "", " ")
|
||||
// log this
|
||||
return func() (any, error) {
|
||||
if jsonerr != nil {
|
||||
return result.Left[any](jsonerr)
|
||||
}
|
||||
log.Printf(prefix, string(b))
|
||||
return result.Of[any](b)
|
||||
}
|
||||
}
|
||||
}
|
||||
42
v2/idiomatic/ioresult/logging_test.go
Normal file
42
v2/idiomatic/ioresult/logging_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// 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 ioresult
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLogging(t *testing.T) {
|
||||
|
||||
type SomeData struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
src := &SomeData{Key: "key", Value: "value"}
|
||||
|
||||
res := F.Pipe1(
|
||||
Of(src),
|
||||
ChainFirst(LogJSON[*SomeData]("Data: \n%s")),
|
||||
)
|
||||
|
||||
dst, err := res()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, src, dst)
|
||||
}
|
||||
132
v2/idiomatic/ioresult/monad.go
Normal file
132
v2/idiomatic/ioresult/monad.go
Normal file
@@ -0,0 +1,132 @@
|
||||
// Copyright (c) 2024 - 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 ioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/monad"
|
||||
"github.com/IBM/fp-go/v2/internal/pointed"
|
||||
)
|
||||
|
||||
type (
|
||||
ioEitherPointed[A any] struct {
|
||||
fof Kleisli[A, A]
|
||||
}
|
||||
|
||||
ioEitherFunctor[A, B any] struct {
|
||||
fmap func(func(A) B) Operator[A, B]
|
||||
}
|
||||
|
||||
ioEitherApply[A, B any] struct {
|
||||
ioEitherFunctor[A, B]
|
||||
fap func(IOResult[A]) Operator[func(A) B, B]
|
||||
}
|
||||
|
||||
ioEitherChainable[A, B any] struct {
|
||||
ioEitherApply[A, B]
|
||||
fchain func(Kleisli[A, B]) Operator[A, B]
|
||||
}
|
||||
|
||||
ioEitherMonad[A, B any] struct {
|
||||
ioEitherPointed[A]
|
||||
ioEitherChainable[A, B]
|
||||
}
|
||||
)
|
||||
|
||||
// Of implements the Pointed interface for IOResult.
|
||||
func (o *ioEitherPointed[A]) Of(a A) IOResult[A] {
|
||||
return o.fof(a)
|
||||
}
|
||||
|
||||
// Map implements the Monad interface's Map operation.
|
||||
func (o *ioEitherFunctor[A, B]) Map(f func(A) B) Operator[A, B] {
|
||||
return o.fmap(f)
|
||||
}
|
||||
|
||||
// Chain implements the Monad interface's Chain operation.
|
||||
func (o *ioEitherChainable[A, B]) Chain(f Kleisli[A, B]) Operator[A, B] {
|
||||
return o.fchain(f)
|
||||
}
|
||||
|
||||
// Ap implements the Monad interface's Ap operation.
|
||||
func (o *ioEitherApply[A, B]) Ap(fa IOResult[A]) Operator[func(A) B, B] {
|
||||
return o.fap(fa)
|
||||
}
|
||||
|
||||
// Pointed implements the pointed operations for [IOEither]
|
||||
// Pointed returns a Pointed instance for IOResult.
|
||||
// Pointed provides the ability to lift pure values into the IOResult context.
|
||||
func Pointed[A any]() pointed.Pointed[A, IOResult[A]] {
|
||||
return &ioEitherPointed[A]{
|
||||
Of[A],
|
||||
}
|
||||
}
|
||||
|
||||
// Functor implements the monadic operations for [IOEither]
|
||||
// Functor returns a Functor instance for IOResult.
|
||||
// Functor provides the Map operation for transforming values.
|
||||
func Functor[A, B any]() functor.Functor[A, B, IOResult[A], IOResult[B]] {
|
||||
return &ioEitherFunctor[A, B]{
|
||||
Map[A, B],
|
||||
}
|
||||
}
|
||||
|
||||
// Monad implements the monadic operations for [IOEither]
|
||||
// Monad returns a Monad instance for IOResult.
|
||||
// Monad provides the full monadic interface including Map, Chain, and Ap.
|
||||
func Monad[A, B any]() monad.Monad[A, B, IOResult[A], IOResult[B], IOResult[func(A) B]] {
|
||||
return MonadPar[A, B]()
|
||||
}
|
||||
|
||||
// Monad implements the monadic operations for [IOEither]
|
||||
// Monad returns a Monad instance for IOResult.
|
||||
// Monad provides the full monadic interface including Map, Chain, and Ap.
|
||||
func MonadPar[A, B any]() monad.Monad[A, B, IOResult[A], IOResult[B], IOResult[func(A) B]] {
|
||||
return &ioEitherMonad[A, B]{
|
||||
ioEitherPointed[A]{
|
||||
Of[A],
|
||||
},
|
||||
ioEitherChainable[A, B]{
|
||||
ioEitherApply[A, B]{
|
||||
ioEitherFunctor[A, B]{
|
||||
Map[A, B],
|
||||
},
|
||||
ApPar[B, A],
|
||||
},
|
||||
Chain[A, B],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Monad implements the monadic operations for [IOEither]
|
||||
// Monad returns a Monad instance for IOResult.
|
||||
// Monad provides the full monadic interface including Map, Chain, and Ap.
|
||||
func MonadSeq[A, B any]() monad.Monad[A, B, IOResult[A], IOResult[B], IOResult[func(A) B]] {
|
||||
return &ioEitherMonad[A, B]{
|
||||
ioEitherPointed[A]{
|
||||
Of[A],
|
||||
},
|
||||
ioEitherChainable[A, B]{
|
||||
ioEitherApply[A, B]{
|
||||
ioEitherFunctor[A, B]{
|
||||
Map[A, B],
|
||||
},
|
||||
ApSeq[B, A],
|
||||
},
|
||||
Chain[A, B],
|
||||
},
|
||||
}
|
||||
}
|
||||
598
v2/idiomatic/ioresult/monad_test.go
Normal file
598
v2/idiomatic/ioresult/monad_test.go
Normal file
@@ -0,0 +1,598 @@
|
||||
// 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 ioresult
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Helper function to compare IOResult values
|
||||
func ioResultEqual[T comparable](a, b IOResult[T]) bool {
|
||||
valA, errA := a()
|
||||
valB, errB := b()
|
||||
|
||||
if errA != nil && errB != nil {
|
||||
return errA.Error() == errB.Error()
|
||||
}
|
||||
if errA != nil || errB != nil {
|
||||
return false
|
||||
}
|
||||
return valA == valB
|
||||
}
|
||||
|
||||
// TestPointedOf tests that Pointed().Of creates a successful IOResult
|
||||
func TestPointedOf(t *testing.T) {
|
||||
t.Run("Creates successful IOResult with integer", func(t *testing.T) {
|
||||
pointed := Pointed[int]()
|
||||
result := pointed.Of(42)
|
||||
|
||||
val, err := result()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, val)
|
||||
})
|
||||
|
||||
t.Run("Creates successful IOResult with string", func(t *testing.T) {
|
||||
pointed := Pointed[string]()
|
||||
result := pointed.Of("hello")
|
||||
|
||||
val, err := result()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello", val)
|
||||
})
|
||||
|
||||
t.Run("Creates successful IOResult with struct", func(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
pointed := Pointed[User]()
|
||||
user := User{Name: "Alice", Age: 30}
|
||||
result := pointed.Of(user)
|
||||
|
||||
val, err := result()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, user, val)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFunctorMap tests that Functor().Map correctly transforms values
|
||||
func TestFunctorMap(t *testing.T) {
|
||||
t.Run("Maps over successful value", func(t *testing.T) {
|
||||
functor := Functor[int, int]()
|
||||
io := Of(5)
|
||||
mapped := functor.Map(N.Mul(2))(io)
|
||||
|
||||
val, err := mapped()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 10, val)
|
||||
})
|
||||
|
||||
t.Run("Maps over error preserves error", func(t *testing.T) {
|
||||
functor := Functor[int, int]()
|
||||
io := Left[int](errors.New("test error"))
|
||||
mapped := functor.Map(N.Mul(2))(io)
|
||||
|
||||
_, err := mapped()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "test error", err.Error())
|
||||
})
|
||||
|
||||
t.Run("Maps with type transformation", func(t *testing.T) {
|
||||
functor := Functor[int, string]()
|
||||
io := Of(42)
|
||||
mapped := functor.Map(func(x int) string { return fmt.Sprintf("value: %d", x) })(io)
|
||||
|
||||
val, err := mapped()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "value: 42", val)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadChain tests that Monad().Chain correctly chains computations
|
||||
func TestMonadChain(t *testing.T) {
|
||||
t.Run("Chains successful computations", func(t *testing.T) {
|
||||
monad := Monad[int, int]()
|
||||
io := monad.Of(5)
|
||||
chained := monad.Chain(func(x int) IOResult[int] {
|
||||
return Of(x * 2)
|
||||
})(io)
|
||||
|
||||
val, err := chained()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 10, val)
|
||||
})
|
||||
|
||||
t.Run("Chains with error in first computation", func(t *testing.T) {
|
||||
monad := Monad[int, int]()
|
||||
io := Left[int](errors.New("initial error"))
|
||||
chained := monad.Chain(func(x int) IOResult[int] {
|
||||
return Of(x * 2)
|
||||
})(io)
|
||||
|
||||
_, err := chained()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "initial error", err.Error())
|
||||
})
|
||||
|
||||
t.Run("Chains with error in second computation", func(t *testing.T) {
|
||||
monad := Monad[int, int]()
|
||||
io := monad.Of(5)
|
||||
chained := monad.Chain(func(x int) IOResult[int] {
|
||||
return Left[int](errors.New("chain error"))
|
||||
})(io)
|
||||
|
||||
_, err := chained()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "chain error", err.Error())
|
||||
})
|
||||
|
||||
t.Run("Chains with type transformation", func(t *testing.T) {
|
||||
monad := Monad[int, string]()
|
||||
io := Of(42)
|
||||
chained := monad.Chain(func(x int) IOResult[string] {
|
||||
return Of(fmt.Sprintf("value: %d", x))
|
||||
})(io)
|
||||
|
||||
val, err := chained()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "value: 42", val)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadAp tests the applicative functionality
|
||||
func TestMonadAp(t *testing.T) {
|
||||
t.Run("Applies function to value", func(t *testing.T) {
|
||||
monad := Monad[int, int]()
|
||||
fn := Of(N.Mul(2))
|
||||
val := monad.Of(5)
|
||||
result := monad.Ap(val)(fn)
|
||||
|
||||
res, err := result()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 10, res)
|
||||
})
|
||||
|
||||
t.Run("Error in function", func(t *testing.T) {
|
||||
monad := Monad[int, int]()
|
||||
fn := Left[func(int) int](errors.New("function error"))
|
||||
val := monad.Of(5)
|
||||
result := monad.Ap(val)(fn)
|
||||
|
||||
_, err := result()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "function error", err.Error())
|
||||
})
|
||||
|
||||
t.Run("Error in value", func(t *testing.T) {
|
||||
monad := Monad[int, int]()
|
||||
fn := Of(N.Mul(2))
|
||||
val := Left[int](errors.New("value error"))
|
||||
result := monad.Ap(val)(fn)
|
||||
|
||||
_, err := result()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "value error", err.Error())
|
||||
})
|
||||
}
|
||||
|
||||
// Monad Law Tests
|
||||
|
||||
// TestMonadLeftIdentity verifies: Chain(Of(a), f) == f(a)
|
||||
// The left identity law states that wrapping a value with Of and then chaining
|
||||
// with a function f should be the same as just applying f to the value.
|
||||
func TestMonadLeftIdentity(t *testing.T) {
|
||||
t.Run("Left identity with successful function", func(t *testing.T) {
|
||||
monad := Monad[int, string]()
|
||||
a := 42
|
||||
f := func(x int) IOResult[string] {
|
||||
return Of(fmt.Sprintf("value: %d", x))
|
||||
}
|
||||
|
||||
// Chain(Of(a), f)
|
||||
left := monad.Chain(f)(monad.Of(a))
|
||||
|
||||
// f(a)
|
||||
right := f(a)
|
||||
|
||||
// Both should produce the same result
|
||||
leftVal, leftErr := left()
|
||||
rightVal, rightErr := right()
|
||||
|
||||
assert.Equal(t, rightErr, leftErr)
|
||||
assert.Equal(t, rightVal, leftVal)
|
||||
})
|
||||
|
||||
t.Run("Left identity with error-returning function", func(t *testing.T) {
|
||||
monad := Monad[int, string]()
|
||||
a := -1
|
||||
f := func(x int) IOResult[string] {
|
||||
if x < 0 {
|
||||
return Left[string](errors.New("negative value"))
|
||||
}
|
||||
return Of(fmt.Sprintf("value: %d", x))
|
||||
}
|
||||
|
||||
left := monad.Chain(f)(monad.Of(a))
|
||||
right := f(a)
|
||||
|
||||
leftVal, leftErr := left()
|
||||
rightVal, rightErr := right()
|
||||
|
||||
assert.Equal(t, rightErr, leftErr)
|
||||
assert.Equal(t, rightVal, leftVal)
|
||||
})
|
||||
|
||||
t.Run("Left identity with multiple values", func(t *testing.T) {
|
||||
testCases := []int{0, 1, 42, 100, -5}
|
||||
monad := Monad[int, int]()
|
||||
f := func(x int) IOResult[int] {
|
||||
return Of(x * 2)
|
||||
}
|
||||
|
||||
for _, a := range testCases {
|
||||
left := monad.Chain(f)(monad.Of(a))
|
||||
right := f(a)
|
||||
|
||||
leftVal, leftErr := left()
|
||||
rightVal, rightErr := right()
|
||||
|
||||
assert.Equal(t, rightErr, leftErr, "Errors should match for value %d", a)
|
||||
assert.Equal(t, rightVal, leftVal, "Values should match for value %d", a)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadRightIdentity verifies: Chain(m, Of) == m
|
||||
// The right identity law states that chaining an IOResult with Of should
|
||||
// return the original IOResult unchanged.
|
||||
func TestMonadRightIdentity(t *testing.T) {
|
||||
t.Run("Right identity with successful value", func(t *testing.T) {
|
||||
monad := Monad[int, int]()
|
||||
m := Of(42)
|
||||
|
||||
// Chain(m, Of)
|
||||
chained := monad.Chain(func(x int) IOResult[int] {
|
||||
return monad.Of(x)
|
||||
})(m)
|
||||
|
||||
// Should be equivalent to m
|
||||
mVal, mErr := m()
|
||||
chainedVal, chainedErr := chained()
|
||||
|
||||
assert.Equal(t, mErr, chainedErr)
|
||||
assert.Equal(t, mVal, chainedVal)
|
||||
})
|
||||
|
||||
t.Run("Right identity with error", func(t *testing.T) {
|
||||
monad := Monad[int, int]()
|
||||
m := Left[int](errors.New("test error"))
|
||||
|
||||
chained := monad.Chain(func(x int) IOResult[int] {
|
||||
return monad.Of(x)
|
||||
})(m)
|
||||
|
||||
mVal, mErr := m()
|
||||
chainedVal, chainedErr := chained()
|
||||
|
||||
assert.Equal(t, mErr, chainedErr)
|
||||
assert.Equal(t, mVal, chainedVal)
|
||||
})
|
||||
|
||||
t.Run("Right identity with different types", func(t *testing.T) {
|
||||
monadStr := Monad[string, string]()
|
||||
m := Of("hello")
|
||||
|
||||
chained := monadStr.Chain(func(x string) IOResult[string] {
|
||||
return monadStr.Of(x)
|
||||
})(m)
|
||||
|
||||
mVal, mErr := m()
|
||||
chainedVal, chainedErr := chained()
|
||||
|
||||
assert.Equal(t, mErr, chainedErr)
|
||||
assert.Equal(t, mVal, chainedVal)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadAssociativity verifies: Chain(Chain(m, f), g) == Chain(m, x => Chain(f(x), g))
|
||||
// The associativity law states that the order of nesting chains doesn't matter.
|
||||
func TestMonadAssociativity(t *testing.T) {
|
||||
t.Run("Associativity with successful computations", func(t *testing.T) {
|
||||
monadIntInt := Monad[int, int]()
|
||||
monadIntStr := Monad[int, string]()
|
||||
|
||||
m := Of(5)
|
||||
f := func(x int) IOResult[int] {
|
||||
return Of(x * 2)
|
||||
}
|
||||
g := func(y int) IOResult[string] {
|
||||
return Of(fmt.Sprintf("result: %d", y))
|
||||
}
|
||||
|
||||
// Chain(Chain(m, f), g)
|
||||
left := monadIntStr.Chain(g)(monadIntInt.Chain(f)(m))
|
||||
|
||||
// Chain(m, x => Chain(f(x), g))
|
||||
right := monadIntStr.Chain(func(x int) IOResult[string] {
|
||||
return monadIntStr.Chain(g)(f(x))
|
||||
})(m)
|
||||
|
||||
leftVal, leftErr := left()
|
||||
rightVal, rightErr := right()
|
||||
|
||||
assert.Equal(t, rightErr, leftErr)
|
||||
assert.Equal(t, rightVal, leftVal)
|
||||
})
|
||||
|
||||
t.Run("Associativity with error in first function", func(t *testing.T) {
|
||||
monadIntInt := Monad[int, int]()
|
||||
monadIntStr := Monad[int, string]()
|
||||
|
||||
m := Of(5)
|
||||
f := func(x int) IOResult[int] {
|
||||
return Left[int](errors.New("error in f"))
|
||||
}
|
||||
g := func(y int) IOResult[string] {
|
||||
return Of(fmt.Sprintf("result: %d", y))
|
||||
}
|
||||
|
||||
left := monadIntStr.Chain(g)(monadIntInt.Chain(f)(m))
|
||||
right := monadIntStr.Chain(func(x int) IOResult[string] {
|
||||
return monadIntStr.Chain(g)(f(x))
|
||||
})(m)
|
||||
|
||||
leftVal, leftErr := left()
|
||||
rightVal, rightErr := right()
|
||||
|
||||
assert.Equal(t, rightErr, leftErr)
|
||||
assert.Equal(t, rightVal, leftVal)
|
||||
})
|
||||
|
||||
t.Run("Associativity with error in second function", func(t *testing.T) {
|
||||
monadIntInt := Monad[int, int]()
|
||||
monadIntStr := Monad[int, string]()
|
||||
|
||||
m := Of(5)
|
||||
f := func(x int) IOResult[int] {
|
||||
return Of(x * 2)
|
||||
}
|
||||
g := func(y int) IOResult[string] {
|
||||
return Left[string](errors.New("error in g"))
|
||||
}
|
||||
|
||||
left := monadIntStr.Chain(g)(monadIntInt.Chain(f)(m))
|
||||
right := monadIntStr.Chain(func(x int) IOResult[string] {
|
||||
return monadIntStr.Chain(g)(f(x))
|
||||
})(m)
|
||||
|
||||
leftVal, leftErr := left()
|
||||
rightVal, rightErr := right()
|
||||
|
||||
assert.Equal(t, rightErr, leftErr)
|
||||
assert.Equal(t, rightVal, leftVal)
|
||||
})
|
||||
|
||||
t.Run("Associativity with complex chain", func(t *testing.T) {
|
||||
monad1 := Monad[int, int]()
|
||||
monad2 := Monad[int, int]()
|
||||
|
||||
m := Of(2)
|
||||
f := func(x int) IOResult[int] { return Of(x + 3) }
|
||||
g := func(y int) IOResult[int] { return Of(y * 4) }
|
||||
|
||||
// (2 + 3) * 4 = 20
|
||||
left := monad2.Chain(g)(monad1.Chain(f)(m))
|
||||
right := monad1.Chain(func(x int) IOResult[int] {
|
||||
return monad2.Chain(g)(f(x))
|
||||
})(m)
|
||||
|
||||
leftVal, leftErr := left()
|
||||
rightVal, rightErr := right()
|
||||
|
||||
assert.NoError(t, leftErr)
|
||||
assert.NoError(t, rightErr)
|
||||
assert.Equal(t, 20, leftVal)
|
||||
assert.Equal(t, 20, rightVal)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFunctorComposition verifies: Map(f . g) == Map(f) . Map(g)
|
||||
// The functor composition law states that mapping a composition of functions
|
||||
// should be the same as composing the maps of those functions.
|
||||
func TestFunctorComposition(t *testing.T) {
|
||||
t.Run("Functor composition law", func(t *testing.T) {
|
||||
functor1 := Functor[int, int]()
|
||||
functor2 := Functor[int, string]()
|
||||
|
||||
m := Of(5)
|
||||
f := N.Mul(2)
|
||||
g := func(x int) string { return fmt.Sprintf("value: %d", x) }
|
||||
|
||||
// Map(g . f)
|
||||
composed := functor2.Map(F.Flow2(f, g))(m)
|
||||
|
||||
// Map(g) . Map(f)
|
||||
separate := functor2.Map(g)(functor1.Map(f)(m))
|
||||
|
||||
composedVal, composedErr := composed()
|
||||
separateVal, separateErr := separate()
|
||||
|
||||
assert.Equal(t, composedErr, separateErr)
|
||||
assert.Equal(t, composedVal, separateVal)
|
||||
})
|
||||
|
||||
t.Run("Functor composition with error", func(t *testing.T) {
|
||||
functor1 := Functor[int, int]()
|
||||
functor2 := Functor[int, string]()
|
||||
|
||||
m := Left[int](errors.New("test error"))
|
||||
f := N.Mul(2)
|
||||
g := func(x int) string { return fmt.Sprintf("value: %d", x) }
|
||||
|
||||
composed := functor2.Map(F.Flow2(f, g))(m)
|
||||
separate := functor2.Map(g)(functor1.Map(f)(m))
|
||||
|
||||
composedVal, composedErr := composed()
|
||||
separateVal, separateErr := separate()
|
||||
|
||||
assert.Equal(t, composedErr, separateErr)
|
||||
assert.Equal(t, composedVal, separateVal)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFunctorIdentity verifies: Map(id) == id
|
||||
// The functor identity law states that mapping the identity function
|
||||
// should return the original IOResult unchanged.
|
||||
func TestFunctorIdentity(t *testing.T) {
|
||||
t.Run("Functor identity with successful value", func(t *testing.T) {
|
||||
functor := Functor[int, int]()
|
||||
m := Of(42)
|
||||
|
||||
// Map(id)
|
||||
mapped := functor.Map(F.Identity[int])(m)
|
||||
|
||||
mVal, mErr := m()
|
||||
mappedVal, mappedErr := mapped()
|
||||
|
||||
assert.Equal(t, mErr, mappedErr)
|
||||
assert.Equal(t, mVal, mappedVal)
|
||||
})
|
||||
|
||||
t.Run("Functor identity with error", func(t *testing.T) {
|
||||
functor := Functor[int, int]()
|
||||
m := Left[int](errors.New("test error"))
|
||||
|
||||
mapped := functor.Map(F.Identity[int])(m)
|
||||
|
||||
mVal, mErr := m()
|
||||
mappedVal, mappedErr := mapped()
|
||||
|
||||
assert.Equal(t, mErr, mappedErr)
|
||||
assert.Equal(t, mVal, mappedVal)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadParVsSeq tests that MonadPar and MonadSeq produce the same results
|
||||
func TestMonadParVsSeq(t *testing.T) {
|
||||
t.Run("Par and Seq produce same results for Map", func(t *testing.T) {
|
||||
monadPar := MonadPar[int, int]()
|
||||
monadSeq := MonadSeq[int, int]()
|
||||
|
||||
io := Of(5)
|
||||
f := N.Mul(2)
|
||||
|
||||
par := monadPar.Map(f)(io)
|
||||
seq := monadSeq.Map(f)(io)
|
||||
|
||||
parVal, parErr := par()
|
||||
seqVal, seqErr := seq()
|
||||
|
||||
assert.Equal(t, parErr, seqErr)
|
||||
assert.Equal(t, parVal, seqVal)
|
||||
})
|
||||
|
||||
t.Run("Par and Seq produce same results for Chain", func(t *testing.T) {
|
||||
monadPar := MonadPar[int, string]()
|
||||
monadSeq := MonadSeq[int, string]()
|
||||
|
||||
io := Of(42)
|
||||
f := func(x int) IOResult[string] {
|
||||
return Of(fmt.Sprintf("value: %d", x))
|
||||
}
|
||||
|
||||
par := monadPar.Chain(f)(io)
|
||||
seq := monadSeq.Chain(f)(io)
|
||||
|
||||
parVal, parErr := par()
|
||||
seqVal, seqErr := seq()
|
||||
|
||||
assert.Equal(t, parErr, seqErr)
|
||||
assert.Equal(t, parVal, seqVal)
|
||||
})
|
||||
|
||||
t.Run("Default Monad uses parallel execution", func(t *testing.T) {
|
||||
monadDefault := Monad[int, int]()
|
||||
monadPar := MonadPar[int, int]()
|
||||
|
||||
io := Of(5)
|
||||
f := N.Mul(2)
|
||||
|
||||
def := monadDefault.Map(f)(io)
|
||||
par := monadPar.Map(f)(io)
|
||||
|
||||
defVal, defErr := def()
|
||||
parVal, parErr := par()
|
||||
|
||||
assert.Equal(t, parErr, defErr)
|
||||
assert.Equal(t, parVal, defVal)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadIntegration tests complete workflows using the monad interface
|
||||
func TestMonadIntegration(t *testing.T) {
|
||||
t.Run("Complex pipeline using monad operations", func(t *testing.T) {
|
||||
monad1 := Monad[int, int]()
|
||||
monad2 := Monad[int, string]()
|
||||
|
||||
// Build a pipeline: multiply by 2, add 3, then format
|
||||
result := F.Pipe2(
|
||||
monad1.Of(5),
|
||||
monad1.Map(N.Mul(2)),
|
||||
monad1.Chain(func(x int) IOResult[int] {
|
||||
return Of(x + 3)
|
||||
}),
|
||||
)
|
||||
|
||||
// Continue with type change
|
||||
formatted := monad2.Map(func(x int) string {
|
||||
return fmt.Sprintf("Final: %d", x)
|
||||
})(result)
|
||||
|
||||
val, err := formatted()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Final: 13", val) // (5 * 2) + 3 = 13
|
||||
})
|
||||
|
||||
t.Run("Error handling in complex pipeline", func(t *testing.T) {
|
||||
monad1 := Monad[int, int]()
|
||||
monad2 := Monad[int, string]()
|
||||
|
||||
result := F.Pipe2(
|
||||
monad1.Of(5),
|
||||
monad1.Map(N.Mul(2)),
|
||||
monad1.Chain(func(x int) IOResult[int] {
|
||||
if x > 5 {
|
||||
return Left[int](errors.New("value too large"))
|
||||
}
|
||||
return Of(x + 3)
|
||||
}),
|
||||
)
|
||||
|
||||
formatted := monad2.Map(func(x int) string {
|
||||
return fmt.Sprintf("Final: %d", x)
|
||||
})(result)
|
||||
|
||||
_, err := formatted()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "value too large", err.Error())
|
||||
})
|
||||
}
|
||||
66
v2/idiomatic/ioresult/monoid.go
Normal file
66
v2/idiomatic/ioresult/monoid.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) 2023 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 ioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
)
|
||||
|
||||
type (
|
||||
Monoid[A any] = monoid.Monoid[IOResult[A]]
|
||||
)
|
||||
|
||||
// ApplicativeMonoid returns a [Monoid] that concatenates [IOEither] instances via their applicative
|
||||
// ApplicativeMonoid returns a Monoid that concatenates IOResult instances via their applicative.
|
||||
// Uses parallel execution (default Ap behavior).
|
||||
func ApplicativeMonoid[A any](
|
||||
m monoid.Monoid[A],
|
||||
) Monoid[A] {
|
||||
return monoid.ApplicativeMonoid(
|
||||
MonadOf[A],
|
||||
MonadMap[A, func(A) A],
|
||||
MonadAp[A, A],
|
||||
m,
|
||||
)
|
||||
}
|
||||
|
||||
// ApplicativeMonoid returns a [Monoid] that concatenates [IOEither] instances via their applicative
|
||||
// ApplicativeMonoidSeq returns a Monoid that concatenates IOResult instances sequentially.
|
||||
// Uses sequential execution (ApSeq).
|
||||
func ApplicativeMonoidSeq[A any](
|
||||
m monoid.Monoid[A],
|
||||
) Monoid[A] {
|
||||
return monoid.ApplicativeMonoid(
|
||||
MonadOf[A],
|
||||
MonadMap[A, func(A) A],
|
||||
MonadApSeq[A, A],
|
||||
m,
|
||||
)
|
||||
}
|
||||
|
||||
// ApplicativeMonoid returns a [Monoid] that concatenates [IOEither] instances via their applicative
|
||||
// ApplicativeMonoidPar returns a Monoid that concatenates IOResult instances in parallel.
|
||||
// Uses parallel execution (ApPar) explicitly.
|
||||
func ApplicativeMonoidPar[A any](
|
||||
m monoid.Monoid[A],
|
||||
) Monoid[A] {
|
||||
return monoid.ApplicativeMonoid(
|
||||
MonadOf[A],
|
||||
MonadMap[A, func(A) A],
|
||||
MonadApPar[A, A],
|
||||
m,
|
||||
)
|
||||
}
|
||||
238
v2/idiomatic/ioresult/monoid_test.go
Normal file
238
v2/idiomatic/ioresult/monoid_test.go
Normal file
@@ -0,0 +1,238 @@
|
||||
// Copyright (c) 2023 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 ioresult
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestApplicativeMonoid(t *testing.T) {
|
||||
m := ApplicativeMonoid(S.Monoid)
|
||||
|
||||
// good cases
|
||||
result1, err1 := m.Concat(Of("a"), Of("b"))()
|
||||
assert.NoError(t, err1)
|
||||
assert.Equal(t, "ab", result1)
|
||||
|
||||
result2, err2 := m.Concat(Of("a"), m.Empty())()
|
||||
assert.NoError(t, err2)
|
||||
assert.Equal(t, "a", result2)
|
||||
|
||||
result3, err3 := m.Concat(m.Empty(), Of("b"))()
|
||||
assert.NoError(t, err3)
|
||||
assert.Equal(t, "b", result3)
|
||||
|
||||
// bad cases
|
||||
e1 := fmt.Errorf("e1")
|
||||
e2 := fmt.Errorf("e2")
|
||||
|
||||
_, err4 := m.Concat(Left[string](e1), Of("b"))()
|
||||
assert.Error(t, err4)
|
||||
assert.Equal(t, e1, err4)
|
||||
|
||||
_, err5 := m.Concat(Left[string](e1), Left[string](e2))()
|
||||
assert.Error(t, err5)
|
||||
assert.Equal(t, e1, err5)
|
||||
|
||||
_, err6 := m.Concat(Of("a"), Left[string](e2))()
|
||||
assert.Error(t, err6)
|
||||
assert.Equal(t, e2, err6)
|
||||
}
|
||||
|
||||
func TestApplicativeMonoidSeq(t *testing.T) {
|
||||
m := ApplicativeMonoidSeq(S.Monoid)
|
||||
|
||||
t.Run("Sequential concatenation of successful values", func(t *testing.T) {
|
||||
result, err := m.Concat(Of("hello"), Of(" world"))()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello world", result)
|
||||
})
|
||||
|
||||
t.Run("Empty element is identity (left)", func(t *testing.T) {
|
||||
result, err := m.Concat(m.Empty(), Of("test"))()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test", result)
|
||||
})
|
||||
|
||||
t.Run("Empty element is identity (right)", func(t *testing.T) {
|
||||
result, err := m.Concat(Of("test"), m.Empty())()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test", result)
|
||||
})
|
||||
|
||||
t.Run("First error short-circuits", func(t *testing.T) {
|
||||
e1 := fmt.Errorf("error1")
|
||||
_, err := m.Concat(Left[string](e1), Of("world"))()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, e1, err)
|
||||
})
|
||||
|
||||
t.Run("Second error after first succeeds", func(t *testing.T) {
|
||||
e2 := fmt.Errorf("error2")
|
||||
_, err := m.Concat(Of("hello"), Left[string](e2))()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, e2, err)
|
||||
})
|
||||
|
||||
t.Run("Multiple concatenations", func(t *testing.T) {
|
||||
m := ApplicativeMonoidSeq(S.Monoid)
|
||||
result, err := m.Concat(
|
||||
m.Concat(Of("a"), Of("b")),
|
||||
m.Concat(Of("c"), Of("d")),
|
||||
)()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "abcd", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeMonoidPar(t *testing.T) {
|
||||
m := ApplicativeMonoidPar(N.MonoidSum[int]())
|
||||
|
||||
t.Run("Parallel concatenation of successful values", func(t *testing.T) {
|
||||
result, err := m.Concat(Of(10), Of(20))()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 30, result)
|
||||
})
|
||||
|
||||
t.Run("Empty element is identity (left)", func(t *testing.T) {
|
||||
result, err := m.Concat(m.Empty(), Of(42))()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
|
||||
t.Run("Empty element is identity (right)", func(t *testing.T) {
|
||||
result, err := m.Concat(Of(42), m.Empty())()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
|
||||
t.Run("Both empty returns empty", func(t *testing.T) {
|
||||
result, err := m.Empty()()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, result) // 0 is the identity for sum
|
||||
})
|
||||
|
||||
t.Run("First error in parallel", func(t *testing.T) {
|
||||
e1 := fmt.Errorf("error1")
|
||||
_, err := m.Concat(Left[int](e1), Of(20))()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, e1, err)
|
||||
})
|
||||
|
||||
t.Run("Second error in parallel", func(t *testing.T) {
|
||||
e2 := fmt.Errorf("error2")
|
||||
_, err := m.Concat(Of(10), Left[int](e2))()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, e2, err)
|
||||
})
|
||||
|
||||
t.Run("Associativity property", func(t *testing.T) {
|
||||
// (a <> b) <> c == a <> (b <> c)
|
||||
a := Of(1)
|
||||
b := Of(2)
|
||||
c := Of(3)
|
||||
|
||||
left, leftErr := m.Concat(m.Concat(a, b), c)()
|
||||
right, rightErr := m.Concat(a, m.Concat(b, c))()
|
||||
|
||||
assert.NoError(t, leftErr)
|
||||
assert.NoError(t, rightErr)
|
||||
assert.Equal(t, 6, left)
|
||||
assert.Equal(t, 6, right)
|
||||
})
|
||||
|
||||
t.Run("Identity property (left)", func(t *testing.T) {
|
||||
// empty <> a == a
|
||||
a := Of(42)
|
||||
result, err := m.Concat(m.Empty(), a)()
|
||||
expected, expectedErr := a()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, expectedErr)
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Identity property (right)", func(t *testing.T) {
|
||||
// a <> empty == a
|
||||
a := Of(42)
|
||||
result, err := m.Concat(a, m.Empty())()
|
||||
expected, expectedErr := a()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, expectedErr)
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonoidWithProduct(t *testing.T) {
|
||||
m := ApplicativeMonoid(N.MonoidProduct[int]())
|
||||
|
||||
t.Run("Multiply successful values", func(t *testing.T) {
|
||||
result, err := m.Concat(Of(3), Of(4))()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 12, result)
|
||||
})
|
||||
|
||||
t.Run("Identity is 1 for product", func(t *testing.T) {
|
||||
result, err := m.Empty()()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, result)
|
||||
})
|
||||
|
||||
t.Run("Multiply with identity", func(t *testing.T) {
|
||||
result, err := m.Concat(Of(5), m.Empty())()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 5, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonoidLaws(t *testing.T) {
|
||||
// Test that all three monoid variants satisfy monoid laws
|
||||
testMonoidLaws := func(name string, m Monoid[string]) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
a := Of("a")
|
||||
b := Of("b")
|
||||
c := Of("c")
|
||||
|
||||
t.Run("Associativity: (a <> b) <> c == a <> (b <> c)", func(t *testing.T) {
|
||||
left, _ := m.Concat(m.Concat(a, b), c)()
|
||||
right, _ := m.Concat(a, m.Concat(b, c))()
|
||||
assert.Equal(t, left, right)
|
||||
})
|
||||
|
||||
t.Run("Left identity: empty <> a == a", func(t *testing.T) {
|
||||
result, _ := m.Concat(m.Empty(), a)()
|
||||
expected, _ := a()
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Right identity: a <> empty == a", func(t *testing.T) {
|
||||
result, _ := m.Concat(a, m.Empty())()
|
||||
expected, _ := a()
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
testMonoidLaws("ApplicativeMonoid", ApplicativeMonoid(S.Monoid))
|
||||
testMonoidLaws("ApplicativeMonoidSeq", ApplicativeMonoidSeq(S.Monoid))
|
||||
testMonoidLaws("ApplicativeMonoidPar", ApplicativeMonoidPar(S.Monoid))
|
||||
}
|
||||
211
v2/idiomatic/ioresult/profile_test.go
Normal file
211
v2/idiomatic/ioresult/profile_test.go
Normal file
@@ -0,0 +1,211 @@
|
||||
// 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 ioresult
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
// Benchmark the closure allocations in Bind
|
||||
func BenchmarkBindAllocations(b *testing.B) {
|
||||
type Data struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
setter := func(v int) func(Data) Data {
|
||||
return func(d Data) Data {
|
||||
d.Value = v
|
||||
return d
|
||||
}
|
||||
}
|
||||
|
||||
b.Run("Bind", func(b *testing.B) {
|
||||
io := Of(Data{Value: 0})
|
||||
f := func(d Data) IOResult[int] { return Of(d.Value * 2) }
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := Bind(setter, f)(io)
|
||||
_, _ = result()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("DirectChainMap", func(b *testing.B) {
|
||||
io := Of(Data{Value: 0})
|
||||
f := func(d Data) IOResult[int] { return Of(d.Value * 2) }
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Manual inlined version of Bind to see baseline
|
||||
result := Chain(func(s1 Data) IOResult[Data] {
|
||||
return Map(func(b int) Data {
|
||||
return setter(b)(s1)
|
||||
})(f(s1))
|
||||
})(io)
|
||||
_, _ = result()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark Map with different patterns
|
||||
func BenchmarkMapPatterns(b *testing.B) {
|
||||
b.Run("SimpleFunction", func(b *testing.B) {
|
||||
io := Of(42)
|
||||
f := N.Mul(2)
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := Map(f)(io)
|
||||
_, _ = result()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("InlinedLambda", func(b *testing.B) {
|
||||
io := Of(42)
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := Map(N.Mul(2))(io)
|
||||
_, _ = result()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("NestedMaps", func(b *testing.B) {
|
||||
io := Of(42)
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := F.Pipe3(
|
||||
io,
|
||||
Map(N.Add(1)),
|
||||
Map(N.Mul(2)),
|
||||
Map(N.Sub(3)),
|
||||
)
|
||||
_, _ = result()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark Of patterns
|
||||
func BenchmarkOfPatterns(b *testing.B) {
|
||||
b.Run("IntValue", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
io := Of(42)
|
||||
_, _ = io()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("StructValue", func(b *testing.B) {
|
||||
type Data struct {
|
||||
A int
|
||||
B string
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
io := Of(Data{A: 42, B: "test"})
|
||||
_, _ = io()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("PointerValue", func(b *testing.B) {
|
||||
type Data struct {
|
||||
A int
|
||||
B string
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
io := Of(&Data{A: 42, B: "test"})
|
||||
_, _ = io()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark the internal chain implementation
|
||||
func BenchmarkChainPatterns(b *testing.B) {
|
||||
b.Run("SimpleChain", func(b *testing.B) {
|
||||
io := Of(42)
|
||||
f := func(x int) IOResult[int] { return Of(x * 2) }
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := Chain(f)(io)
|
||||
_, _ = result()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ChainSequence", func(b *testing.B) {
|
||||
f1 := func(x int) IOResult[int] { return Of(x + 1) }
|
||||
f2 := func(x int) IOResult[int] { return Of(x * 2) }
|
||||
f3 := func(x int) IOResult[int] { return Of(x - 3) }
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := F.Pipe3(
|
||||
Of(42),
|
||||
Chain(f1),
|
||||
Chain(f2),
|
||||
Chain(f3),
|
||||
)
|
||||
_, _ = result()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark error handling paths
|
||||
func BenchmarkErrorPaths(b *testing.B) {
|
||||
b.Run("SuccessPath", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := F.Pipe2(
|
||||
Of(42),
|
||||
Map(N.Mul(2)),
|
||||
Chain(func(x int) IOResult[int] { return Of(x + 1) }),
|
||||
)
|
||||
_, _ = result()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ErrorPath", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := F.Pipe2(
|
||||
Left[int](errors.New("error")),
|
||||
Map(N.Mul(2)),
|
||||
Chain(func(x int) IOResult[int] { return Of(x + 1) }),
|
||||
)
|
||||
_, _ = result()
|
||||
}
|
||||
})
|
||||
}
|
||||
45
v2/idiomatic/ioresult/retry.go
Normal file
45
v2/idiomatic/ioresult/retry.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// 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 ioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
R "github.com/IBM/fp-go/v2/retry"
|
||||
)
|
||||
|
||||
// Retrying retries an IOResult computation according to a retry policy.
|
||||
// The action receives retry status information on each attempt.
|
||||
// The check function determines if the result warrants another retry.
|
||||
func Retrying[A any](
|
||||
policy R.RetryPolicy,
|
||||
action Kleisli[R.RetryStatus, A],
|
||||
check func(A, error) bool,
|
||||
) IOResult[A] {
|
||||
fromResult := io.Retrying(policy,
|
||||
func(rs R.RetryStatus) IO[Result[A]] {
|
||||
return func() Result[A] {
|
||||
return result.TryCatchError(action(rs)())
|
||||
}
|
||||
},
|
||||
func(a Result[A]) bool {
|
||||
return check(result.Unwrap(a))
|
||||
},
|
||||
)
|
||||
return func() (A, error) {
|
||||
return result.Unwrap(fromResult())
|
||||
}
|
||||
}
|
||||
51
v2/idiomatic/ioresult/retry_test.go
Normal file
51
v2/idiomatic/ioresult/retry_test.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// 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 ioresult
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
R "github.com/IBM/fp-go/v2/retry"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var expLogBackoff = R.ExponentialBackoff(10 * time.Millisecond)
|
||||
|
||||
// our retry policy with a 1s cap
|
||||
var testLogPolicy = R.CapDelay(
|
||||
2*time.Second,
|
||||
R.Monoid.Concat(expLogBackoff, R.LimitRetries(20)),
|
||||
)
|
||||
|
||||
func TestRetry(t *testing.T) {
|
||||
action := func(status R.RetryStatus) IOResult[string] {
|
||||
if status.IterNumber < 5 {
|
||||
return Left[string](fmt.Errorf("retrying %d", status.IterNumber))
|
||||
}
|
||||
return Of(fmt.Sprintf("Retrying %d", status.IterNumber))
|
||||
}
|
||||
check := func(val string, err error) bool {
|
||||
return err != nil
|
||||
}
|
||||
|
||||
r := Retrying(testLogPolicy, action, check)
|
||||
|
||||
result, err := r()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Retrying 5", result)
|
||||
}
|
||||
33
v2/idiomatic/ioresult/semigroup.go
Normal file
33
v2/idiomatic/ioresult/semigroup.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// 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 ioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/semigroup"
|
||||
)
|
||||
|
||||
type (
|
||||
Semigroup[A any] = semigroup.Semigroup[IOResult[A]]
|
||||
)
|
||||
|
||||
// AltSemigroup is a [Semigroup] that tries the first item and then the second one using an alternative
|
||||
// AltSemigroup creates a Semigroup that tries the first IOResult, then the second on failure.
|
||||
// This implements the alternative operation for combining IOResults.
|
||||
func AltSemigroup[A any]() Semigroup[A] {
|
||||
return semigroup.AltSemigroup(
|
||||
MonadAlt[A],
|
||||
)
|
||||
}
|
||||
120
v2/idiomatic/ioresult/semigroup_test.go
Normal file
120
v2/idiomatic/ioresult/semigroup_test.go
Normal file
@@ -0,0 +1,120 @@
|
||||
// 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 ioresult
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAltSemigroup(t *testing.T) {
|
||||
sg := AltSemigroup[int]()
|
||||
|
||||
t.Run("First succeeds, second not evaluated", func(t *testing.T) {
|
||||
first := Of(42)
|
||||
second := Of(100)
|
||||
|
||||
result, err := sg.Concat(first, second)()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
|
||||
t.Run("First fails, second succeeds", func(t *testing.T) {
|
||||
first := Left[int](errors.New("first error"))
|
||||
second := Of(100)
|
||||
|
||||
result, err := sg.Concat(first, second)()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 100, result)
|
||||
})
|
||||
|
||||
t.Run("Both fail, returns second error", func(t *testing.T) {
|
||||
first := Left[int](errors.New("first error"))
|
||||
second := Left[int](errors.New("second error"))
|
||||
|
||||
_, err := sg.Concat(first, second)()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "second error", err.Error())
|
||||
})
|
||||
|
||||
t.Run("Both succeed, returns first", func(t *testing.T) {
|
||||
first := Of(42)
|
||||
second := Of(100)
|
||||
|
||||
result, err := sg.Concat(first, second)()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
|
||||
t.Run("Associativity property", func(t *testing.T) {
|
||||
// (a <> b) <> c == a <> (b <> c)
|
||||
a := Left[int](errors.New("a"))
|
||||
b := Left[int](errors.New("b"))
|
||||
c := Of(42)
|
||||
|
||||
// Left associative: (a <> b) <> c
|
||||
left, leftErr := sg.Concat(sg.Concat(a, b), c)()
|
||||
|
||||
// Right associative: a <> (b <> c)
|
||||
right, rightErr := sg.Concat(a, sg.Concat(b, c))()
|
||||
|
||||
assert.NoError(t, leftErr)
|
||||
assert.NoError(t, rightErr)
|
||||
assert.Equal(t, left, right)
|
||||
assert.Equal(t, 42, left)
|
||||
})
|
||||
|
||||
t.Run("Multiple alternatives", func(t *testing.T) {
|
||||
sg := AltSemigroup[string]()
|
||||
|
||||
first := Left[string](errors.New("error1"))
|
||||
second := Left[string](errors.New("error2"))
|
||||
third := Left[string](errors.New("error3"))
|
||||
fourth := Of("success")
|
||||
|
||||
result, err := sg.Concat(
|
||||
sg.Concat(sg.Concat(first, second), third),
|
||||
fourth,
|
||||
)()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "success", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAltSemigroupWithStrings(t *testing.T) {
|
||||
sg := AltSemigroup[string]()
|
||||
|
||||
t.Run("Concatenate successful string results", func(t *testing.T) {
|
||||
first := Of("hello")
|
||||
second := Of("world")
|
||||
|
||||
result, err := sg.Concat(first, second)()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello", result) // First one wins
|
||||
})
|
||||
|
||||
t.Run("Fallback to second on first failure", func(t *testing.T) {
|
||||
first := Left[string](errors.New("network error"))
|
||||
second := Of("fallback value")
|
||||
|
||||
result, err := sg.Concat(first, second)()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "fallback value", result)
|
||||
})
|
||||
}
|
||||
83
v2/idiomatic/ioresult/sequence_test.go
Normal file
83
v2/idiomatic/ioresult/sequence_test.go
Normal file
@@ -0,0 +1,83 @@
|
||||
// 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 ioresult
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
TST "github.com/IBM/fp-go/v2/internal/testing"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMapSeq(t *testing.T) {
|
||||
var results []string
|
||||
|
||||
handler := func(value string) IOResult[string] {
|
||||
return func() (string, error) {
|
||||
results = append(results, value)
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
|
||||
src := A.From("a", "b", "c")
|
||||
|
||||
res := F.Pipe2(
|
||||
src,
|
||||
TraverseArraySeq(handler),
|
||||
Map(func(data []string) bool {
|
||||
return assert.Equal(t, data, results)
|
||||
}),
|
||||
)
|
||||
|
||||
result, err := res()
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, result)
|
||||
}
|
||||
|
||||
func TestSequenceArray(t *testing.T) {
|
||||
|
||||
s := TST.SequenceArrayTest(
|
||||
FromStrictEquals[bool](),
|
||||
Pointed[string](),
|
||||
Pointed[bool](),
|
||||
Functor[[]string, bool](),
|
||||
SequenceArray[string],
|
||||
)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
t.Run(fmt.Sprintf("TestSequenceArray %d", i), s(i))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSequenceArrayError(t *testing.T) {
|
||||
|
||||
s := TST.SequenceArrayErrorTest(
|
||||
FromStrictEquals[bool](),
|
||||
Left[string],
|
||||
Left[bool],
|
||||
Pointed[string](),
|
||||
Pointed[bool](),
|
||||
Functor[[]string, bool](),
|
||||
SequenceArray[string],
|
||||
)
|
||||
// run across four bits
|
||||
s(4)(t)
|
||||
}
|
||||
32
v2/idiomatic/ioresult/sync.go
Normal file
32
v2/idiomatic/ioresult/sync.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// 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 ioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// WithLock executes the provided IO operation in the scope of a lock
|
||||
// WithLock executes an IOResult within the scope of a lock.
|
||||
// The lock is acquired before execution and released after (via defer).
|
||||
func WithLock[A any](lock IO[context.CancelFunc]) Operator[A, A] {
|
||||
return func(fa IOResult[A]) IOResult[A] {
|
||||
return func() (A, error) {
|
||||
defer lock()()
|
||||
return fa()
|
||||
}
|
||||
}
|
||||
}
|
||||
76
v2/idiomatic/ioresult/testing/laws.go
Normal file
76
v2/idiomatic/ioresult/testing/laws.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// 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 testing
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
EQ "github.com/IBM/fp-go/v2/eq"
|
||||
L "github.com/IBM/fp-go/v2/internal/monad/testing"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
)
|
||||
|
||||
// AssertLaws asserts the apply monad laws for the `IOEither` monad
|
||||
func AssertLaws[E, A, B, C any](t *testing.T,
|
||||
eqe EQ.Eq[E],
|
||||
eqa EQ.Eq[A],
|
||||
eqb EQ.Eq[B],
|
||||
eqc EQ.Eq[C],
|
||||
|
||||
ab func(A) B,
|
||||
bc func(B) C,
|
||||
) func(a A) bool {
|
||||
|
||||
return L.AssertLaws(t,
|
||||
ioeither.Eq(either.Eq(eqe, eqa)),
|
||||
ioeither.Eq(either.Eq(eqe, eqb)),
|
||||
ioeither.Eq(either.Eq(eqe, eqc)),
|
||||
|
||||
ioeither.Of[E, A],
|
||||
ioeither.Of[E, B],
|
||||
ioeither.Of[E, C],
|
||||
|
||||
ioeither.Of[E, func(A) A],
|
||||
ioeither.Of[E, func(A) B],
|
||||
ioeither.Of[E, func(B) C],
|
||||
ioeither.Of[E, func(func(A) B) B],
|
||||
|
||||
ioeither.MonadMap[E, A, A],
|
||||
ioeither.MonadMap[E, A, B],
|
||||
ioeither.MonadMap[E, A, C],
|
||||
ioeither.MonadMap[E, B, C],
|
||||
|
||||
ioeither.MonadMap[E, func(B) C, func(func(A) B) func(A) C],
|
||||
|
||||
ioeither.MonadChain[E, A, A],
|
||||
ioeither.MonadChain[E, A, B],
|
||||
ioeither.MonadChain[E, A, C],
|
||||
ioeither.MonadChain[E, B, C],
|
||||
|
||||
ioeither.MonadAp[A, E, A],
|
||||
ioeither.MonadAp[B, E, A],
|
||||
ioeither.MonadAp[C, E, B],
|
||||
ioeither.MonadAp[C, E, A],
|
||||
|
||||
ioeither.MonadAp[B, E, func(A) B],
|
||||
ioeither.MonadAp[func(A) C, E, func(A) B],
|
||||
|
||||
ab,
|
||||
bc,
|
||||
)
|
||||
|
||||
}
|
||||
48
v2/idiomatic/ioresult/testing/laws_test.go
Normal file
48
v2/idiomatic/ioresult/testing/laws_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
// 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 testing
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
EQ "github.com/IBM/fp-go/v2/eq"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMonadLaws(t *testing.T) {
|
||||
// some comparison
|
||||
eqe := EQ.FromStrictEquals[string]()
|
||||
eqa := EQ.FromStrictEquals[bool]()
|
||||
eqb := EQ.FromStrictEquals[int]()
|
||||
eqc := EQ.FromStrictEquals[string]()
|
||||
|
||||
ab := func(a bool) int {
|
||||
if a {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
bc := func(b int) string {
|
||||
return fmt.Sprintf("value %d", b)
|
||||
}
|
||||
|
||||
laws := AssertLaws(t, eqe, eqa, eqb, eqc, ab, bc)
|
||||
|
||||
assert.True(t, laws(true))
|
||||
assert.True(t, laws(false))
|
||||
}
|
||||
234
v2/idiomatic/ioresult/traverse.go
Normal file
234
v2/idiomatic/ioresult/traverse.go
Normal file
@@ -0,0 +1,234 @@
|
||||
// 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 ioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/array"
|
||||
"github.com/IBM/fp-go/v2/internal/record"
|
||||
)
|
||||
|
||||
// TraverseArray transforms an array
|
||||
// TraverseArray transforms an array by applying an IOResult-producing function to each element.
|
||||
// Uses parallel execution by default. If any element fails, the entire traversal fails.
|
||||
//
|
||||
//go:inline
|
||||
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
return array.Traverse[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
Ap[[]B, B],
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// TraverseArrayWithIndex transforms an array
|
||||
// TraverseArrayWithIndex transforms an array with access to element indices.
|
||||
// Uses parallel execution by default.
|
||||
//
|
||||
//go:inline
|
||||
func TraverseArrayWithIndex[A, B any](f func(int, A) IOResult[B]) Kleisli[[]A, []B] {
|
||||
return array.TraverseWithIndex[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
Ap[[]B, B],
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// SequenceArray converts a homogeneous sequence of either into an either of sequence
|
||||
// SequenceArray converts an array of IOResults into an IOResult of an array.
|
||||
// Uses parallel execution by default.
|
||||
func SequenceArray[A any](ma []IOResult[A]) IOResult[[]A] {
|
||||
return TraverseArray(function.Identity[IOResult[A]])(ma)
|
||||
}
|
||||
|
||||
// TraverseRecord transforms a record
|
||||
// TraverseRecord transforms a map by applying an IOResult-producing function to each value.
|
||||
// Uses parallel execution by default.
|
||||
//
|
||||
//go:inline
|
||||
func TraverseRecord[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, map[K]B] {
|
||||
return record.Traverse[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
Ap[map[K]B, B],
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// TraverseRecordWithIndex transforms a record
|
||||
// TraverseRecordWithIndex transforms a map with access to keys.
|
||||
// Uses parallel execution by default.
|
||||
//
|
||||
//go:inline
|
||||
func TraverseRecordWithIndex[K comparable, A, B any](f func(K, A) IOResult[B]) Kleisli[map[K]A, map[K]B] {
|
||||
return record.TraverseWithIndex[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
Ap[map[K]B, B],
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// SequenceRecord converts a homogeneous sequence of either into an either of sequence
|
||||
// SequenceRecord converts a map of IOResults into an IOResult of a map.
|
||||
// Uses parallel execution by default.
|
||||
func SequenceRecord[K comparable, A any](ma map[K]IOResult[A]) IOResult[map[K]A] {
|
||||
return TraverseRecord[K](function.Identity[IOResult[A]])(ma)
|
||||
}
|
||||
|
||||
// TraverseArraySeq transforms an array
|
||||
// TraverseArraySeq transforms an array sequentially.
|
||||
// Elements are processed one at a time in order.
|
||||
//
|
||||
//go:inline
|
||||
func TraverseArraySeq[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
return array.Traverse[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
ApSeq[[]B, B],
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// TraverseArrayWithIndexSeq transforms an array
|
||||
// TraverseArrayWithIndexSeq transforms an array sequentially with indices.
|
||||
//
|
||||
//go:inline
|
||||
func TraverseArrayWithIndexSeq[A, B any](f func(int, A) IOResult[B]) Kleisli[[]A, []B] {
|
||||
return array.TraverseWithIndex[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
ApSeq[[]B, B],
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// SequenceArraySeq converts a homogeneous sequence of either into an either of sequence
|
||||
// SequenceArraySeq converts an array of IOResults sequentially.
|
||||
func SequenceArraySeq[A any](ma []IOResult[A]) IOResult[[]A] {
|
||||
return TraverseArraySeq(function.Identity[IOResult[A]])(ma)
|
||||
}
|
||||
|
||||
// TraverseRecordSeq transforms a record
|
||||
// TraverseRecordSeq transforms a map sequentially.
|
||||
//
|
||||
//go:inline
|
||||
func TraverseRecordSeq[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, map[K]B] {
|
||||
return record.Traverse[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
ApSeq[map[K]B, B],
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// TraverseRecordWithIndexSeq transforms a record
|
||||
// TraverseRecordWithIndexSeq transforms a map sequentially with keys.
|
||||
//
|
||||
//go:inline
|
||||
func TraverseRecordWithIndexSeq[K comparable, A, B any](f func(K, A) IOResult[B]) Kleisli[map[K]A, map[K]B] {
|
||||
return record.TraverseWithIndex[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
ApSeq[map[K]B, B],
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// SequenceRecordSeq converts a homogeneous sequence of either into an either of sequence
|
||||
// SequenceRecordSeq converts a map of IOResults sequentially.
|
||||
func SequenceRecordSeq[K comparable, A any](ma map[K]IOResult[A]) IOResult[map[K]A] {
|
||||
return TraverseRecordSeq[K](function.Identity[IOResult[A]])(ma)
|
||||
}
|
||||
|
||||
// TraverseArrayPar transforms an array
|
||||
// TraverseArrayPar transforms an array in parallel (explicit).
|
||||
// This is equivalent to TraverseArray but makes parallelism explicit.
|
||||
//
|
||||
//go:inline
|
||||
func TraverseArrayPar[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
return array.Traverse[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
ApPar[[]B, B],
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// TraverseArrayWithIndexPar transforms an array
|
||||
// TraverseArrayWithIndexPar transforms an array in parallel with indices (explicit).
|
||||
//
|
||||
//go:inline
|
||||
func TraverseArrayWithIndexPar[A, B any](f func(int, A) IOResult[B]) Kleisli[[]A, []B] {
|
||||
return array.TraverseWithIndex[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
ApPar[[]B, B],
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// SequenceArrayPar converts a homogeneous Paruence of either into an either of Paruence
|
||||
// SequenceArrayPar converts an array of IOResults in parallel (explicit).
|
||||
func SequenceArrayPar[A any](ma []IOResult[A]) IOResult[[]A] {
|
||||
return TraverseArrayPar(function.Identity[IOResult[A]])(ma)
|
||||
}
|
||||
|
||||
// TraverseRecordPar transforms a record
|
||||
// TraverseRecordPar transforms a map in parallel (explicit).
|
||||
//
|
||||
//go:inline
|
||||
func TraverseRecordPar[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, map[K]B] {
|
||||
return record.Traverse[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
ApPar[map[K]B, B],
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// TraverseRecordWithIndexPar transforms a record
|
||||
// TraverseRecordWithIndexPar transforms a map in parallel with keys (explicit).
|
||||
//
|
||||
//go:inline
|
||||
func TraverseRecordWithIndexPar[K comparable, A, B any](f func(K, A) IOResult[B]) Kleisli[map[K]A, map[K]B] {
|
||||
return record.TraverseWithIndex[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
ApSeq[map[K]B, B],
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// SequenceRecordPar converts a homogeneous Paruence of either into an either of Paruence
|
||||
// SequenceRecordPar converts a map of IOResults in parallel (explicit).
|
||||
func SequenceRecordPar[K comparable, A any](ma map[K]IOResult[A]) IOResult[map[K]A] {
|
||||
return TraverseRecordPar[K](function.Identity[IOResult[A]])(ma)
|
||||
}
|
||||
38
v2/idiomatic/ioresult/traverse_test.go
Normal file
38
v2/idiomatic/ioresult/traverse_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
// 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 ioresult
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTraverseArray(t *testing.T) {
|
||||
|
||||
src := A.From("A", "B")
|
||||
|
||||
trfrm := TraverseArrayWithIndex(func(idx int, data string) IOResult[string] {
|
||||
return Of(fmt.Sprintf("idx: %d, data: %s", idx, data))
|
||||
})
|
||||
|
||||
result, err := trfrm(src)()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, A.From("idx: 0, data: A", "idx: 1, data: B"), result)
|
||||
|
||||
}
|
||||
39
v2/idiomatic/ioresult/types.go
Normal file
39
v2/idiomatic/ioresult/types.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package ioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
type (
|
||||
// IO represents a computation that performs side effects and returns a value of type A.
|
||||
IO[A any] = io.IO[A]
|
||||
|
||||
// Lazy represents a deferred computation that produces a value of type A when evaluated.
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
|
||||
// Result represents an Either with error as the left type, compatible with Go's (value, error) tuple.
|
||||
Result[A any] = result.Result[A]
|
||||
|
||||
// Reader represents a computation that depends on a read-only environment of type R and produces a value of type A.
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
|
||||
// Endomorphism represents a function from type A to type A.
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
// IOResult represents a computation that performs IO and may fail with an error.
|
||||
// It follows Go's idiomatic pattern of returning (value, error) tuples.
|
||||
// A successful computation returns (value, nil), while a failed one returns (zero, error).
|
||||
IOResult[A any] = func() (A, error)
|
||||
|
||||
// Kleisli represents a function from A to an IOResult of B.
|
||||
// It is used for chaining computations that may fail.
|
||||
Kleisli[A, B any] = Reader[A, IOResult[B]]
|
||||
|
||||
// Operator represents a transformation from IOResult[A] to IOResult[B].
|
||||
// It is commonly used in function composition pipelines.
|
||||
Operator[A, B any] = Kleisli[IOResult[A], B]
|
||||
)
|
||||
@@ -17,6 +17,8 @@ package option
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
// Benchmark basic construction
|
||||
@@ -46,7 +48,7 @@ func BenchmarkIsSome(b *testing.B) {
|
||||
|
||||
func BenchmarkMap(b *testing.B) {
|
||||
v, ok := Some(21)
|
||||
mapper := Map(func(x int) int { return x * 2 })
|
||||
mapper := Map(N.Mul(2))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
||||
@@ -17,6 +17,8 @@ package option
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
// Benchmark shallow chain (1 step)
|
||||
@@ -81,11 +83,11 @@ func BenchmarkMap_5Steps(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
v1, ok1 := Map(func(x int) int { return x + 1 })(v, ok)
|
||||
v2, ok2 := Map(func(x int) int { return x * 3 })(v1, ok1)
|
||||
v3, ok3 := Map(func(x int) int { return x + 20 })(v2, ok2)
|
||||
v4, ok4 := Map(func(x int) int { return x / 2 })(v3, ok3)
|
||||
_, _ = Map(func(x int) int { return x - 10 })(v4, ok4)
|
||||
v1, ok1 := Map(N.Add(1))(v, ok)
|
||||
v2, ok2 := Map(N.Mul(3))(v1, ok1)
|
||||
v3, ok3 := Map(N.Add(20))(v2, ok2)
|
||||
v4, ok4 := Map(N.Div(2))(v3, ok3)
|
||||
_, _ = Map(N.Sub(10))(v4, ok4)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
//
|
||||
// Map transforms the contained value:
|
||||
//
|
||||
// double := Map(func(x int) int { return x * 2 })
|
||||
// double := Map(N.Mul(2))
|
||||
// result := double(Some(21)) // (42, true)
|
||||
// result := double(None[int]()) // (0, false)
|
||||
//
|
||||
@@ -113,7 +113,7 @@
|
||||
//
|
||||
// Applicative example:
|
||||
//
|
||||
// fab := Some(func(x int) int { return x * 2 })
|
||||
// fab := Some(N.Mul(2))
|
||||
// fa := Some(21)
|
||||
// result := Ap[int](fa)(fab) // (42, true)
|
||||
//
|
||||
|
||||
@@ -1,15 +1,39 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package option
|
||||
|
||||
// Pipe1 takes an initial value t0 and successively applies 1 functions where the input of a function is the return value of the previous function
|
||||
// The final return value is the result of the last function application
|
||||
// Pipe1 takes an initial value t0 and successively applies 1 function where the input of a function is the return value of the previous function.
|
||||
// The final return value is the result of the last function application.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := Pipe1(42, func(x int) (int, bool) { return x * 2, true }) // (84, true)
|
||||
//
|
||||
//go:inline
|
||||
func Pipe1[F1 ~func(T0) (T1, bool), T0, T1 any](t0 T0, f1 F1) (T1, bool) {
|
||||
return f1(t0)
|
||||
}
|
||||
|
||||
// Flow1 creates a function that takes an initial value t0 and successively applies 1 functions where the input of a function is the return value of the previous function
|
||||
// The final return value is the result of the last function application
|
||||
// Flow1 creates a function that takes an initial value t0 and successively applies 1 function where the input of a function is the return value of the previous function.
|
||||
// The final return value is the result of the last function application.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// double := Flow1(func(x int, ok bool) (int, bool) { return x * 2, ok })
|
||||
// result := double(42, true) // (84, true)
|
||||
//
|
||||
//go:inline
|
||||
func Flow1[F1 ~func(T0, bool) (T1, bool), T0, T1 any](f1 F1) func(T0, bool) (T1, bool) {
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/eq"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -206,13 +207,13 @@ func TestFlow5(t *testing.T) {
|
||||
func TestMakeFunctor(t *testing.T) {
|
||||
t.Run("Map with functor", func(t *testing.T) {
|
||||
f := MakeFunctor[int, int]()
|
||||
double := f.Map(func(x int) int { return x * 2 })
|
||||
double := f.Map(N.Mul(2))
|
||||
AssertEq(Some(42))(double(Some(21)))(t)
|
||||
})
|
||||
|
||||
t.Run("Map with None", func(t *testing.T) {
|
||||
f := MakeFunctor[int, int]()
|
||||
double := f.Map(func(x int) int { return x * 2 })
|
||||
double := f.Map(N.Mul(2))
|
||||
AssertEq(None[int]())(double(None[int]()))(t)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
// result, ok := none // ok == false, result == 0
|
||||
//
|
||||
// // Transforming Options
|
||||
// doubled := Map(func(x int) int { return x * 2 })(some) // (84, true)
|
||||
// doubled := Map(N.Mul(2))(some) // (84, true)
|
||||
package option
|
||||
|
||||
import (
|
||||
@@ -56,16 +56,47 @@ func FromPredicate[A any](pred func(A) bool) Kleisli[A, A] {
|
||||
}
|
||||
}
|
||||
|
||||
// FromZero returns a function that creates an Option based on whether a value is the zero value.
|
||||
// Returns Some if the value is the zero value, None otherwise.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// checkZero := FromZero[int]()
|
||||
// result := checkZero(0) // Some(0)
|
||||
// result := checkZero(5) // None
|
||||
//
|
||||
//go:inline
|
||||
func FromZero[A comparable]() Kleisli[A, A] {
|
||||
return FromPredicate(P.IsZero[A]())
|
||||
}
|
||||
|
||||
// FromNonZero returns a function that creates an Option based on whether a value is non-zero.
|
||||
// Returns Some if the value is non-zero, None otherwise.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// checkNonZero := FromNonZero[int]()
|
||||
// result := checkNonZero(5) // Some(5)
|
||||
// result := checkNonZero(0) // None
|
||||
//
|
||||
//go:inline
|
||||
func FromNonZero[A comparable]() Kleisli[A, A] {
|
||||
return FromPredicate(P.IsNonZero[A]())
|
||||
}
|
||||
|
||||
// FromEq returns a function that creates an Option based on equality with a given value.
|
||||
// The returned function takes a value to compare against and returns a Kleisli function.
|
||||
//
|
||||
// Parameters:
|
||||
// - pred: An equality predicate
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import "github.com/IBM/fp-go/v2/eq"
|
||||
// equals42 := FromEq(eq.FromStrictEquals[int]())(42)
|
||||
// result := equals42(42) // Some(42)
|
||||
// result := equals42(10) // None
|
||||
//
|
||||
//go:inline
|
||||
func FromEq[A any](pred eq.Eq[A]) func(A) Kleisli[A, A] {
|
||||
return F.Flow2(P.IsEqual(pred), FromPredicate[A])
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package option
|
||||
|
||||
import (
|
||||
@@ -7,6 +22,10 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
Seq[T any] = iter.Seq[T]
|
||||
// Seq is an iterator sequence type alias for working with Go 1.23+ iterators.
|
||||
Seq[T any] = iter.Seq[T]
|
||||
|
||||
// Endomorphism represents a function from type T to type T.
|
||||
// It is commonly used for transformations that preserve the type.
|
||||
Endomorphism[T any] = endomorphism.Endomorphism[T]
|
||||
)
|
||||
|
||||
112
v2/idiomatic/readerioresult/doc.go
Normal file
112
v2/idiomatic/readerioresult/doc.go
Normal file
@@ -0,0 +1,112 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package readerioresult provides a ReaderIOResult monad that combines Reader, IO, and Result monads.
|
||||
//
|
||||
// A ReaderIOResult[R, A] represents a computation that:
|
||||
// - Depends on an environment of type R (Reader aspect)
|
||||
// - Performs IO operations (IO aspect)
|
||||
// - May fail with an error (Result aspect, which is Either[error, A])
|
||||
//
|
||||
// This is equivalent to Reader[R, IOResult[A]] or Reader[R, func() (A, error)].
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// ReaderIOResult is particularly useful for:
|
||||
//
|
||||
// 1. Dependency injection with IO and error handling - pass configuration/services through
|
||||
// computations that perform side effects and may fail
|
||||
// 2. Functional IO with context - compose IO operations that depend on environment and may error
|
||||
// 3. Testing - easily mock dependencies and IO operations by changing the environment value
|
||||
// 4. Resource management - manage resources that depend on configuration
|
||||
//
|
||||
// # Basic Example
|
||||
//
|
||||
// type Config struct {
|
||||
// DatabaseURL string
|
||||
// Timeout time.Duration
|
||||
// }
|
||||
//
|
||||
// // Function that needs config, performs IO, and may fail
|
||||
// func fetchUser(id int) readerioresult.ReaderIOResult[Config, User] {
|
||||
// return func(cfg Config) ioresult.IOResult[User] {
|
||||
// return func() (User, error) {
|
||||
// // Use cfg.DatabaseURL and cfg.Timeout to fetch user
|
||||
// return queryDatabase(cfg.DatabaseURL, id, cfg.Timeout)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Execute by providing the config
|
||||
// cfg := Config{DatabaseURL: "postgres://...", Timeout: 5 * time.Second}
|
||||
// ioResult := fetchUser(42)(cfg) // Returns IOResult[User]
|
||||
// user, err := ioResult() // Execute the IO operation
|
||||
//
|
||||
// # Composition
|
||||
//
|
||||
// ReaderIOResult provides several ways to compose computations:
|
||||
//
|
||||
// 1. Map - transform successful values
|
||||
// 2. Chain (FlatMap) - sequence dependent IO operations
|
||||
// 3. Ap - combine independent IO computations
|
||||
// 4. ChainFirst - perform IO for side effects while keeping original value
|
||||
//
|
||||
// # Example with Composition
|
||||
//
|
||||
// type AppContext struct {
|
||||
// DB *sql.DB
|
||||
// Cache Cache
|
||||
// Log Logger
|
||||
// }
|
||||
//
|
||||
// getUserWithCache := F.Pipe2(
|
||||
// getFromCache(userID),
|
||||
// readerioresult.Alt(func() readerioresult.ReaderIOResult[AppContext, User] {
|
||||
// return F.Pipe2(
|
||||
// getFromDB(userID),
|
||||
// readerioresult.ChainFirst(saveToCache),
|
||||
// )
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
// ctx := AppContext{DB: db, Cache: cache, Log: logger}
|
||||
// user, err := getUserWithCache(ctx)()
|
||||
//
|
||||
// # Error Handling
|
||||
//
|
||||
// ReaderIOResult provides several functions for error handling:
|
||||
//
|
||||
// - Left/Right - create failed/successful values
|
||||
// - GetOrElse - provide a default value for errors
|
||||
// - OrElse - recover from errors with an alternative computation
|
||||
// - Fold - handle both success and failure cases
|
||||
// - ChainLeft - transform error values into new computations
|
||||
//
|
||||
// # Relationship to Other Monads
|
||||
//
|
||||
// ReaderIOResult is related to several other monads in this library:
|
||||
//
|
||||
// - Reader[R, A] - ReaderIOResult without IO or error handling
|
||||
// - IOResult[A] - ReaderIOResult without environment dependency
|
||||
// - ReaderResult[R, A] - ReaderIOResult without IO (pure computations)
|
||||
// - ReaderIO[R, A] - ReaderIOResult without error handling
|
||||
// - ReaderIOEither[R, E, A] - like ReaderIOResult but with custom error type E
|
||||
//
|
||||
// # Performance Note
|
||||
//
|
||||
// ReaderIOResult is a zero-cost abstraction - it compiles to a simple function type
|
||||
// with no runtime overhead beyond the underlying computation. The IO operations are
|
||||
// lazy and only executed when the final IOResult is called.
|
||||
package readerioresult
|
||||
66
v2/idiomatic/readerioresult/types.go
Normal file
66
v2/idiomatic/readerioresult/types.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
type (
|
||||
// Endomorphism represents a function from type A to type A.
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
// Lazy represents a deferred computation that produces a value of type A when evaluated.
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
|
||||
// Option represents an optional value that may or may not be present.
|
||||
Option[A any] = option.Option[A]
|
||||
|
||||
// Result represents an Either with error as the left type, compatible with Go's (value, error) tuple.
|
||||
Result[A any] = result.Result[A]
|
||||
|
||||
// Reader represents a computation that depends on a read-only environment of type R and produces a value of type A.
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
|
||||
// IO represents a computation that performs side effects and returns a value of type A.
|
||||
IO[A any] = io.IO[A]
|
||||
|
||||
// IOResult represents a computation that performs IO and may fail with an error.
|
||||
IOResult[A any] = ioresult.IOResult[A]
|
||||
|
||||
// ReaderIOResult represents a computation that depends on an environment R,
|
||||
// performs IO operations, and may fail with an error.
|
||||
// It is equivalent to Reader[R, IOResult[A]] or func(R) func() (A, error).
|
||||
ReaderIOResult[R, A any] = Reader[R, IOResult[A]]
|
||||
|
||||
// Monoid represents a monoid structure for ReaderIOResult values.
|
||||
Monoid[R, A any] = monoid.Monoid[ReaderIOResult[R, A]]
|
||||
|
||||
// Kleisli represents a function from A to a ReaderIOResult of B.
|
||||
// It is used for chaining computations that depend on environment, perform IO, and may fail.
|
||||
Kleisli[R, A, B any] = Reader[A, ReaderIOResult[R, B]]
|
||||
|
||||
// Operator represents a transformation from ReaderIOResult[R, A] to ReaderIOResult[R, B].
|
||||
// It is commonly used in function composition pipelines.
|
||||
Operator[R, A, B any] = Kleisli[R, ReaderIOResult[R, A], B]
|
||||
)
|
||||
94
v2/idiomatic/readerresult/array.go
Normal file
94
v2/idiomatic/readerresult/array.go
Normal file
@@ -0,0 +1,94 @@
|
||||
// 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 (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/array"
|
||||
)
|
||||
|
||||
// TraverseArray applies a ReaderResult-returning function to each element of an array,
|
||||
// collecting the results. If any element fails, the entire operation fails with the first error.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// parseUser := func(id int) readerresult.ReaderResult[DB, User] { ... }
|
||||
// ids := []int{1, 2, 3}
|
||||
// result := readerresult.TraverseArray[DB](parseUser)(ids)
|
||||
// // result(db) returns ([]User, nil) with all users or (nil, error) on first error
|
||||
//
|
||||
//go:inline
|
||||
func TraverseArray[R, A, B any](f Kleisli[R, A, B]) Kleisli[R, []A, []B] {
|
||||
return array.Traverse[[]A](
|
||||
Of[R, []B],
|
||||
Map[R, []B, func(B) []B],
|
||||
Ap[[]B, R, B],
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadTraverseArray[R, A, B any](as []A, f Kleisli[R, A, B]) ReaderResult[R, []B] {
|
||||
return array.MonadTraverse[[]A](
|
||||
Of[R, []B],
|
||||
Map[R, []B, func(B) []B],
|
||||
Ap[[]B, R, B],
|
||||
as,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// TraverseArrayWithIndex is like TraverseArray but the function also receives the element's index.
|
||||
// This is useful when the transformation depends on the position in the array.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// processItem := func(idx int, item string) readerresult.ReaderResult[Config, int] {
|
||||
// return readerresult.Of[Config](idx + len(item))
|
||||
// }
|
||||
// items := []string{"a", "bb", "ccc"}
|
||||
// result := readerresult.TraverseArrayWithIndex[Config](processItem)(items)
|
||||
//
|
||||
//go:inline
|
||||
func TraverseArrayWithIndex[R, A, B any](f func(int, A) ReaderResult[R, B]) Kleisli[R, []A, []B] {
|
||||
return array.TraverseWithIndex[[]A](
|
||||
Of[R, []B],
|
||||
Map[R, []B, func(B) []B],
|
||||
Ap[[]B, R, B],
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// SequenceArray converts an array of ReaderResult values into a single ReaderResult of an array.
|
||||
// If any element fails, the entire operation fails with the first error encountered.
|
||||
// All computations share the same environment.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// readers := []readerresult.ReaderResult[Config, int]{
|
||||
// readerresult.Of[Config](1),
|
||||
// readerresult.Of[Config](2),
|
||||
// readerresult.Of[Config](3),
|
||||
// }
|
||||
// result := readerresult.SequenceArray(readers)
|
||||
// // result(cfg) returns ([]int{1, 2, 3}, nil)
|
||||
//
|
||||
//go:inline
|
||||
func SequenceArray[R, A any](ma []ReaderResult[R, A]) ReaderResult[R, []A] {
|
||||
return MonadTraverseArray(ma, F.Identity[ReaderResult[R, A]])
|
||||
}
|
||||
42
v2/idiomatic/readerresult/array_test.go
Normal file
42
v2/idiomatic/readerresult/array_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// 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"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSequenceArray(t *testing.T) {
|
||||
|
||||
n := 10
|
||||
|
||||
readers := A.MakeBy(n, Of[context.Context, int])
|
||||
exp := A.MakeBy(n, F.Identity[int])
|
||||
|
||||
g := F.Pipe1(
|
||||
readers,
|
||||
SequenceArray[context.Context, int],
|
||||
)
|
||||
|
||||
v, err := g(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, exp, v)
|
||||
}
|
||||
284
v2/idiomatic/readerresult/benchmark_test.go
Normal file
284
v2/idiomatic/readerresult/benchmark_test.go
Normal file
@@ -0,0 +1,284 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
type BenchContext struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
// Benchmark basic operations
|
||||
|
||||
func BenchmarkOf(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
rr := Of[BenchContext](i)
|
||||
_, _ = rr(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLeft(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
err := testError
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
rr := Left[BenchContext, int](err)
|
||||
_, _ = rr(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMap(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr := Of[BenchContext](10)
|
||||
double := N.Mul(2)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
mapped := F.Pipe1(rr, Map[BenchContext](double))
|
||||
_, _ = mapped(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMapChain(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr := Of[BenchContext](1)
|
||||
double := N.Mul(2)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := F.Pipe3(
|
||||
rr,
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChain(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr := Of[BenchContext](10)
|
||||
addOne := func(x int) ReaderResult[BenchContext, int] {
|
||||
return Of[BenchContext](x + 1)
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
chained := F.Pipe1(rr, Chain(addOne))
|
||||
_, _ = chained(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChainDeep(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr := Of[BenchContext](1)
|
||||
addOne := func(x int) ReaderResult[BenchContext, int] {
|
||||
return Of[BenchContext](x + 1)
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := F.Pipe5(
|
||||
rr,
|
||||
Chain(addOne),
|
||||
Chain(addOne),
|
||||
Chain(addOne),
|
||||
Chain(addOne),
|
||||
Chain(addOne),
|
||||
)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAp(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
fab := Of[BenchContext](N.Mul(2))
|
||||
fa := Of[BenchContext](21)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := MonadAp(fab, fa)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSequenceT2(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr1 := Of[BenchContext](10)
|
||||
rr2 := Of[BenchContext](20)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := SequenceT2(rr1, rr2)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSequenceT4(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
rr1 := Of[BenchContext](10)
|
||||
rr2 := Of[BenchContext](20)
|
||||
rr3 := Of[BenchContext](30)
|
||||
rr4 := Of[BenchContext](40)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := SequenceT4(rr1, rr2, rr3, rr4)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDoNotation(b *testing.B) {
|
||||
ctx := context.Background()
|
||||
|
||||
type State struct {
|
||||
A int
|
||||
B int
|
||||
C int
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := F.Pipe3(
|
||||
Do[context.Context](State{}),
|
||||
Bind(
|
||||
func(a int) func(State) State {
|
||||
return func(s State) State { s.A = a; return s }
|
||||
},
|
||||
func(s State) ReaderResult[context.Context, int] {
|
||||
return Of[context.Context](10)
|
||||
},
|
||||
),
|
||||
Bind(
|
||||
func(b int) func(State) State {
|
||||
return func(s State) State { s.B = b; return s }
|
||||
},
|
||||
func(s State) ReaderResult[context.Context, int] {
|
||||
return Of[context.Context](s.A * 2)
|
||||
},
|
||||
),
|
||||
Bind(
|
||||
func(c int) func(State) State {
|
||||
return func(s State) State { s.C = c; return s }
|
||||
},
|
||||
func(s State) ReaderResult[context.Context, int] {
|
||||
return Of[context.Context](s.A + s.B)
|
||||
},
|
||||
),
|
||||
)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkErrorPropagation(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
err := testError
|
||||
rr := Left[BenchContext, int](err)
|
||||
double := N.Mul(2)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := F.Pipe5(
|
||||
rr,
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
Map[BenchContext](double),
|
||||
)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTraverseArray(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
arr := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
kleisli := func(x int) ReaderResult[BenchContext, int] {
|
||||
return Of[BenchContext](x * 2)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
traversed := TraverseArray[BenchContext](kleisli)
|
||||
result := traversed(arr)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSequenceArray(b *testing.B) {
|
||||
ctx := BenchContext{Value: 42}
|
||||
arr := []ReaderResult[BenchContext, int]{
|
||||
Of[BenchContext](1),
|
||||
Of[BenchContext](2),
|
||||
Of[BenchContext](3),
|
||||
Of[BenchContext](4),
|
||||
Of[BenchContext](5),
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := SequenceArray(arr)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Real-world scenario benchmarks
|
||||
|
||||
func BenchmarkRealWorldPipeline(b *testing.B) {
|
||||
type Config struct {
|
||||
Multiplier int
|
||||
Offset int
|
||||
}
|
||||
|
||||
ctx := Config{Multiplier: 5, Offset: 10}
|
||||
|
||||
type State struct {
|
||||
Input int
|
||||
Result int
|
||||
}
|
||||
|
||||
getMultiplier := func(cfg Config) int { return cfg.Multiplier }
|
||||
getOffset := func(cfg Config) int { return cfg.Offset }
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
step1 := Bind(
|
||||
func(m int) func(State) State {
|
||||
return func(s State) State { s.Result = s.Input * m; return s }
|
||||
},
|
||||
func(s State) ReaderResult[Config, int] {
|
||||
return Asks(getMultiplier)
|
||||
},
|
||||
)
|
||||
step2 := Bind(
|
||||
func(off int) func(State) State {
|
||||
return func(s State) State { s.Result += off; return s }
|
||||
},
|
||||
func(s State) ReaderResult[Config, int] {
|
||||
return Asks(getOffset)
|
||||
},
|
||||
)
|
||||
result := F.Pipe3(
|
||||
Do[Config](State{Input: 10}),
|
||||
step1,
|
||||
step2,
|
||||
Map[Config](func(s State) int { return s.Result }),
|
||||
)
|
||||
_, _ = result(ctx)
|
||||
}
|
||||
}
|
||||
663
v2/idiomatic/readerresult/bind.go
Normal file
663
v2/idiomatic/readerresult/bind.go
Normal file
@@ -0,0 +1,663 @@
|
||||
// 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 (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/idiomatic/result"
|
||||
AP "github.com/IBM/fp-go/v2/internal/apply"
|
||||
C "github.com/IBM/fp-go/v2/internal/chain"
|
||||
FE "github.com/IBM/fp-go/v2/internal/fromeither"
|
||||
FR "github.com/IBM/fp-go/v2/internal/fromreader"
|
||||
FC "github.com/IBM/fp-go/v2/internal/functor"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
RES "github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// Do creates an empty context of type [S] to be used with the [Bind] operation.
|
||||
// This is the starting point for do-notation style composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
// type Env struct {
|
||||
// UserService UserService
|
||||
// ConfigService ConfigService
|
||||
// }
|
||||
// result := readereither.Do[Env, error](State{})
|
||||
//
|
||||
//go:inline
|
||||
func Do[R, S any](
|
||||
empty S,
|
||||
) ReaderResult[R, S] {
|
||||
return Of[R](empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// This enables sequential composition where each step can depend on the results of previous steps
|
||||
// and access the shared environment.
|
||||
//
|
||||
// The setter function takes the result of the computation and returns a function that
|
||||
// updates the context from S1 to S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
// type Env struct {
|
||||
// UserService UserService
|
||||
// ConfigService ConfigService
|
||||
// }
|
||||
//
|
||||
// result := function.Pipe2(
|
||||
// readereither.Do[Env, error](State{}),
|
||||
// readereither.Bind(
|
||||
// func(user User) func(State) State {
|
||||
// return func(s State) State { s.User = user; return s }
|
||||
// },
|
||||
// func(s State) readereither.ReaderResult[Env, error, User] {
|
||||
// return readereither.Asks(func(env Env) either.Either[error, User] {
|
||||
// return env.UserService.GetUser()
|
||||
// })
|
||||
// },
|
||||
// ),
|
||||
// readereither.Bind(
|
||||
// func(cfg Config) func(State) State {
|
||||
// return func(s State) State { s.Config = cfg; return s }
|
||||
// },
|
||||
// func(s State) readereither.ReaderResult[Env, error, Config] {
|
||||
// // This can access s.User from the previous step
|
||||
// return readereither.Asks(func(env Env) either.Either[error, Config] {
|
||||
// return env.ConfigService.GetConfigForUser(s.User.ID)
|
||||
// })
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func Bind[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f Kleisli[R, S1, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return C.Bind(
|
||||
Chain[R, S1, S2],
|
||||
Map[R, T, S2],
|
||||
setter,
|
||||
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,
|
||||
) Operator[R, S1, S2] {
|
||||
return FC.Let(
|
||||
Map[R, S1, S2],
|
||||
setter,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// LetTo attaches the a value to a context [S1] to produce a context [S2]
|
||||
//
|
||||
//go:inline
|
||||
func LetTo[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
) func(ReaderResult[R, S1]) ReaderResult[R, S2] {
|
||||
return FC.LetTo(
|
||||
Map[R, S1, S2],
|
||||
setter,
|
||||
b,
|
||||
)
|
||||
}
|
||||
|
||||
// BindTo initializes a new state [S1] from a value [T]
|
||||
//
|
||||
//go:inline
|
||||
func BindTo[R, S1, T any](
|
||||
setter func(T) S1,
|
||||
) Operator[R, T, S1] {
|
||||
return C.BindTo(
|
||||
Map[R, T, S1],
|
||||
setter,
|
||||
)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
// the context and the value concurrently (using Applicative rather than Monad).
|
||||
// This allows independent computations to be combined without one depending on the result of the other.
|
||||
//
|
||||
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
|
||||
// and can conceptually run in parallel.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
// type Env struct {
|
||||
// UserService UserService
|
||||
// ConfigService ConfigService
|
||||
// }
|
||||
//
|
||||
// // These operations are independent and can be combined with ApS
|
||||
// getUser := readereither.Asks(func(env Env) either.Either[error, User] {
|
||||
// return env.UserService.GetUser()
|
||||
// })
|
||||
// getConfig := readereither.Asks(func(env Env) either.Either[error, Config] {
|
||||
// return env.ConfigService.GetConfig()
|
||||
// })
|
||||
//
|
||||
// result := function.Pipe2(
|
||||
// readereither.Do[Env, error](State{}),
|
||||
// readereither.ApS(
|
||||
// func(user User) func(State) State {
|
||||
// return func(s State) State { s.User = user; return s }
|
||||
// },
|
||||
// getUser,
|
||||
// ),
|
||||
// readereither.ApS(
|
||||
// func(cfg Config) func(State) State {
|
||||
// return func(s State) State { s.Config = cfg; return s }
|
||||
// },
|
||||
// getConfig,
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApS[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa ReaderResult[R, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return AP.ApS(
|
||||
Ap[S2, R, T],
|
||||
Map[R, S1, func(T) S2],
|
||||
setter,
|
||||
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.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// This eliminates the need to manually write setter functions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
// type Env struct {
|
||||
// UserService UserService
|
||||
// ConfigService ConfigService
|
||||
// }
|
||||
//
|
||||
// configLens := lens.MakeLens(
|
||||
// func(s State) Config { return s.Config },
|
||||
// func(s State, c Config) State { s.Config = c; return s },
|
||||
// )
|
||||
//
|
||||
// getConfig := readereither.Asks(func(env Env) either.Either[error, Config] {
|
||||
// return env.ConfigService.GetConfig()
|
||||
// })
|
||||
// result := function.Pipe2(
|
||||
// readereither.Of[Env, error](State{}),
|
||||
// readereither.ApSL(configLens, getConfig),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApSL[R, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa ReaderResult[R, T],
|
||||
) Operator[R, S, S] {
|
||||
return ApS(lens.Set, 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.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. The function f receives the current value of the focused field and
|
||||
// returns a ReaderEither computation that produces an updated value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
// type Env struct {
|
||||
// UserService UserService
|
||||
// ConfigService ConfigService
|
||||
// }
|
||||
//
|
||||
// userLens := lens.MakeLens(
|
||||
// func(s State) User { return s.User },
|
||||
// func(s State, u User) State { s.User = u; return s },
|
||||
// )
|
||||
//
|
||||
// result := function.Pipe2(
|
||||
// readereither.Do[Env, error](State{}),
|
||||
// readereither.BindL(userLens, func(user User) readereither.ReaderResult[Env, error, User] {
|
||||
// return readereither.Asks(func(env Env) either.Either[error, User] {
|
||||
// return env.UserService.GetUser()
|
||||
// })
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func BindL[R, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Kleisli[R, T, T],
|
||||
) Operator[R, S, S] {
|
||||
return Bind(lens.Set, function.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetL is a variant of Let that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. The function f receives the current value of the focused field and
|
||||
// returns a new value (without wrapping in a ReaderEither).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// configLens := lens.MakeLens(
|
||||
// func(s State) Config { return s.Config },
|
||||
// func(s State, c Config) State { s.Config = c; return s },
|
||||
// )
|
||||
//
|
||||
// result := function.Pipe2(
|
||||
// readereither.Do[any, error](State{Config: Config{Host: "localhost"}}),
|
||||
// readereither.LetL(configLens, func(cfg Config) Config {
|
||||
// cfg.Port = 8080
|
||||
// return cfg
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func LetL[R, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Endomorphism[T],
|
||||
) Operator[R, S, S] {
|
||||
return Let[R](lens.Set, function.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetToL is a variant of LetTo that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. The value b is set directly to the focused field.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// configLens := lens.MakeLens(
|
||||
// func(s State) Config { return s.Config },
|
||||
// func(s State, c Config) State { s.Config = c; return s },
|
||||
// )
|
||||
//
|
||||
// newConfig := Config{Host: "localhost", Port: 8080}
|
||||
// result := function.Pipe2(
|
||||
// readereither.Do[any, error](State{}),
|
||||
// readereither.LetToL(configLens, newConfig),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func LetToL[R, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
b T,
|
||||
) Operator[R, S, S] {
|
||||
return LetTo[R](lens.Set, b)
|
||||
}
|
||||
|
||||
// BindReaderK lifts a Reader Kleisli arrow into a ReaderResult context and binds it to the state.
|
||||
// This allows you to integrate pure Reader computations (that don't have error handling)
|
||||
// into a ReaderResult computation chain.
|
||||
//
|
||||
// The function f takes the current state S1 and returns a Reader[R, T] computation.
|
||||
// The result T is then attached to the state using the setter to produce state S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Env struct {
|
||||
// ConfigPath string
|
||||
// }
|
||||
// type State struct {
|
||||
// Config string
|
||||
// }
|
||||
//
|
||||
// // A pure Reader computation that reads from environment
|
||||
// getConfigPath := func(s State) reader.Reader[Env, string] {
|
||||
// return func(env Env) string {
|
||||
// return env.ConfigPath
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// result := function.Pipe2(
|
||||
// readerresult.Do[Env](State{}),
|
||||
// readerresult.BindReaderK(
|
||||
// func(path string) func(State) State {
|
||||
// return func(s State) State { s.Config = path; return s }
|
||||
// },
|
||||
// getConfigPath,
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func BindReaderK[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f reader.Kleisli[R, S1, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return FR.BindReaderK(
|
||||
Chain[R, S1, S2],
|
||||
Map[R, T, S2],
|
||||
FromReader[R, T],
|
||||
setter,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindEitherK[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f RES.Kleisli[S1, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return FE.BindEitherK(
|
||||
Chain[R, S1, S2],
|
||||
Map[R, T, S2],
|
||||
FromEither[R, T],
|
||||
setter,
|
||||
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.
|
||||
//
|
||||
// The function f takes the current state S1 and returns a Result[T] computation.
|
||||
// If the Result is successful, the value T is attached to the state using the setter to produce state S2.
|
||||
// If the Result is an error, the entire computation short-circuits with that error.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Value int
|
||||
// ParsedValue int
|
||||
// }
|
||||
//
|
||||
// // A Result computation that may fail
|
||||
// parseValue := func(s State) result.Result[int] {
|
||||
// if s.Value < 0 {
|
||||
// return result.Error[int](errors.New("negative value"))
|
||||
// }
|
||||
// return result.Of(s.Value * 2)
|
||||
// }
|
||||
//
|
||||
// result := function.Pipe2(
|
||||
// readerresult.Do[any](State{Value: 5}),
|
||||
// readerresult.BindResultK(
|
||||
// func(parsed int) func(State) State {
|
||||
// return func(s State) State { s.ParsedValue = parsed; return s }
|
||||
// },
|
||||
// parseValue,
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func BindResultK[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f result.Kleisli[S1, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return C.Bind(
|
||||
Chain[R, S1, S2],
|
||||
Map[R, T, S2],
|
||||
setter,
|
||||
func(s1 S1) ReaderResult[R, T] {
|
||||
return FromResult[R](f(s1))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// The setter function takes the result T from the Reader and initializes the state S1.
|
||||
// This is useful when you want to begin a do-notation chain with a Reader computation
|
||||
// that doesn't involve error handling.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Env struct {
|
||||
// ConfigPath string
|
||||
// }
|
||||
// type State struct {
|
||||
// Config string
|
||||
// }
|
||||
//
|
||||
// // A Reader that just reads from the environment
|
||||
// getConfigPath := func(env Env) string {
|
||||
// return env.ConfigPath
|
||||
// }
|
||||
//
|
||||
// result := function.Pipe1(
|
||||
// reader.Of[Env](getConfigPath),
|
||||
// readerresult.BindToReader(func(path string) State {
|
||||
// return State{Config: path}
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func BindToReader[
|
||||
R, S1, T any](
|
||||
setter func(T) S1,
|
||||
) func(Reader[R, T]) ReaderResult[R, S1] {
|
||||
return function.Flow2(
|
||||
FromReader[R],
|
||||
BindTo[R](setter),
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindToEither[
|
||||
R, S1, T any](
|
||||
setter func(T) S1,
|
||||
) func(Result[T]) ReaderResult[R, S1] {
|
||||
return function.Flow2(
|
||||
FromEither[R],
|
||||
BindTo[R](setter),
|
||||
)
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// The setter function takes the successful result T and initializes the state S1.
|
||||
// If the Result is an error, the entire computation will carry that error forward.
|
||||
// This is useful when you want to begin a do-notation chain with a Result computation
|
||||
// that doesn't need environment access.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// // A Result that might contain an error
|
||||
// parseResult := result.TryCatch(func() int {
|
||||
// // some parsing logic that might fail
|
||||
// return 42
|
||||
// })
|
||||
//
|
||||
// computation := function.Pipe1(
|
||||
// parseResult,
|
||||
// readerresult.BindToResult[any](func(value int) State {
|
||||
// return State{Value: value}
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func BindToResult[
|
||||
R, S1, T any](
|
||||
setter func(T) S1,
|
||||
) func(T, error) ReaderResult[R, S1] {
|
||||
bt := BindTo[R](setter)
|
||||
return func(t T, err error) ReaderResult[R, S1] {
|
||||
return bt(FromResult[R](t, err))
|
||||
}
|
||||
}
|
||||
|
||||
// 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).
|
||||
//
|
||||
// Unlike BindReaderK which uses monadic bind (sequential), ApReaderS uses applicative apply,
|
||||
// meaning the Reader computation fa is independent of the current state and can conceptually
|
||||
// execute in parallel.
|
||||
//
|
||||
// This is useful when you want to combine a Reader computation with your ReaderResult state
|
||||
// without creating a dependency between them.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Env struct {
|
||||
// DefaultPort int
|
||||
// DefaultHost string
|
||||
// }
|
||||
// type State struct {
|
||||
// Port int
|
||||
// Host string
|
||||
// }
|
||||
//
|
||||
// getDefaultPort := func(env Env) int { return env.DefaultPort }
|
||||
// getDefaultHost := func(env Env) string { return env.DefaultHost }
|
||||
//
|
||||
// result := function.Pipe2(
|
||||
// readerresult.Do[Env](State{}),
|
||||
// readerresult.ApReaderS(
|
||||
// func(port int) func(State) State {
|
||||
// return func(s State) State { s.Port = port; return s }
|
||||
// },
|
||||
// getDefaultPort,
|
||||
// ),
|
||||
// readerresult.ApReaderS(
|
||||
// func(host string) func(State) State {
|
||||
// return func(s State) State { s.Host = host; return s }
|
||||
// },
|
||||
// getDefaultHost,
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApReaderS[
|
||||
R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Reader[R, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return ApS(
|
||||
setter,
|
||||
FromReader[R](fa),
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApResultS[
|
||||
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 ApS(
|
||||
setter,
|
||||
FromResult[R](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).
|
||||
//
|
||||
// Unlike BindResultK which uses monadic bind (sequential), ApResultS uses applicative apply,
|
||||
// meaning the Result computation fa is independent of the current state and can conceptually
|
||||
// execute in parallel.
|
||||
//
|
||||
// If the Result fa contains an error, the entire computation short-circuits with that error.
|
||||
// This is useful when you want to combine a Result value with your ReaderResult state
|
||||
// without creating a dependency between them.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Value1 int
|
||||
// Value2 int
|
||||
// }
|
||||
//
|
||||
// // Independent Result computations
|
||||
// parseValue1 := result.TryCatch(func() int { return 42 })
|
||||
// parseValue2 := result.TryCatch(func() int { return 100 })
|
||||
//
|
||||
// computation := function.Pipe2(
|
||||
// readerresult.Do[any](State{}),
|
||||
// readerresult.ApResultS(
|
||||
// func(v int) func(State) State {
|
||||
// return func(s State) State { s.Value1 = v; return s }
|
||||
// },
|
||||
// parseValue1,
|
||||
// ),
|
||||
// readerresult.ApResultS(
|
||||
// func(v int) func(State) State {
|
||||
// return func(s State) State { s.Value2 = v; return s }
|
||||
// },
|
||||
// parseValue2,
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApEitherS[
|
||||
R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Result[T],
|
||||
) Operator[R, S1, S2] {
|
||||
return ApS(
|
||||
setter,
|
||||
FromEither[R](fa),
|
||||
)
|
||||
}
|
||||
338
v2/idiomatic/readerresult/bind_test.go
Normal file
338
v2/idiomatic/readerresult/bind_test.go
Normal file
@@ -0,0 +1,338 @@
|
||||
// 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"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func getLastName(s utils.Initial) ReaderResult[context.Context, string] {
|
||||
return Of[context.Context]("Doe")
|
||||
}
|
||||
|
||||
func getGivenName(s utils.WithLastName) ReaderResult[context.Context, string] {
|
||||
return Of[context.Context]("John")
|
||||
}
|
||||
|
||||
func TestBind(t *testing.T) {
|
||||
|
||||
res := F.Pipe3(
|
||||
Do[context.Context](utils.Empty),
|
||||
Bind(utils.SetLastName, getLastName),
|
||||
Bind(utils.SetGivenName, getGivenName),
|
||||
Map[context.Context](utils.GetFullName),
|
||||
)
|
||||
|
||||
v, err := res(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "John Doe", v)
|
||||
}
|
||||
|
||||
func TestApS(t *testing.T) {
|
||||
|
||||
res := F.Pipe3(
|
||||
Do[context.Context](utils.Empty),
|
||||
ApS(utils.SetLastName, Of[context.Context]("Doe")),
|
||||
ApS(utils.SetGivenName, Of[context.Context]("John")),
|
||||
Map[context.Context](utils.GetFullName),
|
||||
)
|
||||
|
||||
v, err := res(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "John Doe", v)
|
||||
}
|
||||
|
||||
func TestBindReaderK(t *testing.T) {
|
||||
type Env struct {
|
||||
ConfigPath string
|
||||
}
|
||||
type State struct {
|
||||
Config string
|
||||
}
|
||||
|
||||
// A pure Reader computation
|
||||
getConfigPath := func(s State) func(Env) string {
|
||||
return func(env Env) string {
|
||||
return env.ConfigPath
|
||||
}
|
||||
}
|
||||
|
||||
res := F.Pipe2(
|
||||
Do[Env](State{}),
|
||||
BindReaderK(
|
||||
func(path string) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Config = path
|
||||
return s
|
||||
}
|
||||
},
|
||||
getConfigPath,
|
||||
),
|
||||
Map[Env](func(s State) string { return s.Config }),
|
||||
)
|
||||
|
||||
env := Env{ConfigPath: "/etc/config"}
|
||||
v, err := res(env)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "/etc/config", v)
|
||||
}
|
||||
|
||||
func TestBindResultK(t *testing.T) {
|
||||
type State struct {
|
||||
Value int
|
||||
ParsedValue int
|
||||
}
|
||||
|
||||
// A Result computation that may fail
|
||||
parseValue := func(s State) (int, error) {
|
||||
if s.Value < 0 {
|
||||
return 0, assert.AnError
|
||||
}
|
||||
return s.Value * 2, nil
|
||||
}
|
||||
|
||||
t.Run("success case", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[context.Context](State{Value: 5}),
|
||||
BindResultK[context.Context](
|
||||
func(parsed int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.ParsedValue = parsed
|
||||
return s
|
||||
}
|
||||
},
|
||||
parseValue,
|
||||
),
|
||||
Map[context.Context](func(s State) int { return s.ParsedValue }),
|
||||
)
|
||||
|
||||
v, err := res(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 10, v)
|
||||
})
|
||||
|
||||
t.Run("error case", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[context.Context](State{Value: -5}),
|
||||
BindResultK[context.Context](
|
||||
func(parsed int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.ParsedValue = parsed
|
||||
return s
|
||||
}
|
||||
},
|
||||
parseValue,
|
||||
),
|
||||
Map[context.Context](func(s State) int { return s.ParsedValue }),
|
||||
)
|
||||
|
||||
_, err := res(context.Background())
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindToReader(t *testing.T) {
|
||||
type Env struct {
|
||||
ConfigPath string
|
||||
}
|
||||
type State struct {
|
||||
Config string
|
||||
}
|
||||
|
||||
// A Reader that just reads from the environment
|
||||
getConfigPath := func(env Env) string {
|
||||
return env.ConfigPath
|
||||
}
|
||||
|
||||
res := F.Pipe2(
|
||||
getConfigPath,
|
||||
BindToReader[Env](func(path string) State {
|
||||
return State{Config: path}
|
||||
}),
|
||||
Map[Env](func(s State) string { return s.Config }),
|
||||
)
|
||||
|
||||
env := Env{ConfigPath: "/etc/config"}
|
||||
v, err := res(env)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "/etc/config", v)
|
||||
}
|
||||
|
||||
func TestBindToResult(t *testing.T) {
|
||||
type State struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
t.Run("success case", func(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
BindToResult[context.Context](func(value int) State {
|
||||
return State{Value: value}
|
||||
})(42, nil),
|
||||
Map[context.Context](func(s State) int { return s.Value }),
|
||||
)
|
||||
|
||||
v, err := computation(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, v)
|
||||
})
|
||||
|
||||
t.Run("error case", func(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
BindToResult[context.Context](func(value int) State {
|
||||
return State{Value: value}
|
||||
})(0, assert.AnError),
|
||||
Map[context.Context](func(s State) int { return s.Value }),
|
||||
)
|
||||
|
||||
_, err := computation(context.Background())
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApReaderS(t *testing.T) {
|
||||
type Env struct {
|
||||
DefaultPort int
|
||||
DefaultHost string
|
||||
}
|
||||
type State struct {
|
||||
Port int
|
||||
Host string
|
||||
}
|
||||
|
||||
getDefaultPort := func(env Env) int { return env.DefaultPort }
|
||||
getDefaultHost := func(env Env) string { return env.DefaultHost }
|
||||
|
||||
res := F.Pipe3(
|
||||
Do[Env](State{}),
|
||||
ApReaderS(
|
||||
func(port int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Port = port
|
||||
return s
|
||||
}
|
||||
},
|
||||
getDefaultPort,
|
||||
),
|
||||
ApReaderS(
|
||||
func(host string) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Host = host
|
||||
return s
|
||||
}
|
||||
},
|
||||
getDefaultHost,
|
||||
),
|
||||
Map[Env](func(s State) State { return s }),
|
||||
)
|
||||
|
||||
env := Env{DefaultPort: 8080, DefaultHost: "localhost"}
|
||||
state, err := res(env)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 8080, state.Port)
|
||||
assert.Equal(t, "localhost", state.Host)
|
||||
}
|
||||
|
||||
func TestApResultS(t *testing.T) {
|
||||
type State struct {
|
||||
Value1 int
|
||||
Value2 int
|
||||
}
|
||||
|
||||
t.Run("success case", func(t *testing.T) {
|
||||
computation := F.Pipe3(
|
||||
Do[context.Context](State{}),
|
||||
ApResultS[context.Context](
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value1 = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
)(42, nil),
|
||||
ApResultS[context.Context](
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value2 = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
)(100, nil),
|
||||
Map[context.Context](func(s State) State { return s }),
|
||||
)
|
||||
|
||||
state, err := computation(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, state.Value1)
|
||||
assert.Equal(t, 100, state.Value2)
|
||||
})
|
||||
|
||||
t.Run("error in first value", func(t *testing.T) {
|
||||
computation := F.Pipe3(
|
||||
Do[context.Context](State{}),
|
||||
ApResultS[context.Context](
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value1 = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
)(0, assert.AnError),
|
||||
ApResultS[context.Context](
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value2 = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
)(100, nil),
|
||||
Map[context.Context](func(s State) State { return s }),
|
||||
)
|
||||
|
||||
_, err := computation(context.Background())
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("error in second value", func(t *testing.T) {
|
||||
computation := F.Pipe3(
|
||||
Do[context.Context](State{}),
|
||||
ApResultS[context.Context](
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value1 = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
)(42, nil),
|
||||
ApResultS[context.Context](
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value2 = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
)(0, assert.AnError),
|
||||
Map[context.Context](func(s State) State { return s }),
|
||||
)
|
||||
|
||||
_, err := computation(context.Background())
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
125
v2/idiomatic/readerresult/curry.go
Normal file
125
v2/idiomatic/readerresult/curry.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// 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
|
||||
|
||||
// These functions curry/uncurry Go functions with context as the first parameter into/from ReaderResult form.
|
||||
// This follows the Go convention of putting context as the first parameter as advised in https://pkg.go.dev/context.
|
||||
//
|
||||
// Unlike the From* functions which return partially applied functions, Curry* functions return fully curried
|
||||
// functions where each parameter is applied one at a time.
|
||||
|
||||
// Curry0 converts a context-only function into a ReaderResult (same as From0 but emphasizes immediate application).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getConfig := func(ctx context.Context) (Config, error) { ... }
|
||||
// rr := readerresult.Curry0(getConfig)
|
||||
// // rr is a ReaderResult[context.Context, Config]
|
||||
func Curry0[R, A any](f func(R) (A, error)) ReaderResult[R, A] {
|
||||
return f
|
||||
}
|
||||
|
||||
// Curry1 converts a function with one parameter into a curried function returning a ReaderResult.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getUser := func(ctx context.Context, id int) (User, error) { ... }
|
||||
// curried := readerresult.Curry1(getUser)
|
||||
// // curried(42) returns ReaderResult[context.Context, User]
|
||||
func Curry1[R, T1, A any](f func(R, T1) (A, error)) func(T1) ReaderResult[R, A] {
|
||||
return func(t T1) ReaderResult[R, A] {
|
||||
return func(r R) (A, error) {
|
||||
return f(r, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Curry2 converts a function with two parameters into a fully curried function.
|
||||
// Each parameter is applied one at a time.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// queryDB := func(ctx context.Context, table string, id int) (Record, error) { ... }
|
||||
// curried := readerresult.Curry2(queryDB)
|
||||
// // curried("users")(42) returns ReaderResult[context.Context, Record]
|
||||
func Curry2[R, T1, T2, A any](f func(R, T1, T2) (A, error)) func(T1) func(T2) ReaderResult[R, A] {
|
||||
return func(t1 T1) func(T2) ReaderResult[R, A] {
|
||||
return func(t2 T2) ReaderResult[R, A] {
|
||||
return func(r R) (A, error) {
|
||||
return f(r, t1, t2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Curry3 converts a function with three parameters into a fully curried function.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// updateRecord := func(ctx context.Context, table string, id int, data string) (Result, error) { ... }
|
||||
// curried := readerresult.Curry3(updateRecord)
|
||||
// // curried("users")(42)("data") returns ReaderResult[context.Context, Result]
|
||||
func Curry3[R, T1, T2, T3, A any](f func(R, T1, T2, T3) (A, error)) func(T1) func(T2) func(T3) ReaderResult[R, A] {
|
||||
return func(t1 T1) func(T2) func(T3) ReaderResult[R, A] {
|
||||
return func(t2 T2) func(T3) ReaderResult[R, A] {
|
||||
return func(t3 T3) ReaderResult[R, A] {
|
||||
return func(r R) (A, error) {
|
||||
return f(r, t1, t2, t3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Uncurry1 converts a ReaderResult-returning function back into an idiomatic Go function.
|
||||
// This is useful for adapting functional code to work with traditional Go APIs.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rrf := func(id int) readerresult.ReaderResult[context.Context, User] { ... }
|
||||
// gofunc := readerresult.Uncurry1(rrf)
|
||||
// // gofunc(ctx, 42) returns (User, error)
|
||||
func Uncurry1[R, T1, A any](f func(T1) ReaderResult[R, A]) func(R, T1) (A, error) {
|
||||
return func(r R, t T1) (A, error) {
|
||||
return f(t)(r)
|
||||
}
|
||||
}
|
||||
|
||||
// Uncurry2 converts a curried two-parameter ReaderResult function into an idiomatic Go function.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rrf := func(table string) func(int) readerresult.ReaderResult[context.Context, Record] { ... }
|
||||
// gofunc := readerresult.Uncurry2(rrf)
|
||||
// // gofunc(ctx, "users", 42) returns (Record, error)
|
||||
func Uncurry2[R, T1, T2, A any](f func(T1) func(T2) ReaderResult[R, A]) func(R, T1, T2) (A, error) {
|
||||
return func(r R, t1 T1, t2 T2) (A, error) {
|
||||
return f(t1)(t2)(r)
|
||||
}
|
||||
}
|
||||
|
||||
// Uncurry3 converts a curried three-parameter ReaderResult function into an idiomatic Go function.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rrf := func(table string) func(int) func(string) readerresult.ReaderResult[context.Context, Result] { ... }
|
||||
// gofunc := readerresult.Uncurry3(rrf)
|
||||
// // gofunc(ctx, "users", 42, "data") returns (Result, error)
|
||||
func Uncurry3[R, T1, T2, T3, A any](f func(T1) func(T2) func(T3) ReaderResult[R, A]) func(R, T1, T2, T3) (A, error) {
|
||||
return func(r R, t1 T1, t2 T2, t3 T3) (A, error) {
|
||||
return f(t1)(t2)(t3)(r)
|
||||
}
|
||||
}
|
||||
184
v2/idiomatic/readerresult/curry_test.go
Normal file
184
v2/idiomatic/readerresult/curry_test.go
Normal file
@@ -0,0 +1,184 @@
|
||||
// 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"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCurry0(t *testing.T) {
|
||||
getConfig := func(ctx context.Context) (string, error) {
|
||||
return "config", nil
|
||||
}
|
||||
|
||||
rr := Curry0(getConfig)
|
||||
v, err := rr(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "config", v)
|
||||
}
|
||||
|
||||
func TestCurry1(t *testing.T) {
|
||||
getUser := func(ctx context.Context, id int) (string, error) {
|
||||
if id == 42 {
|
||||
return "Alice", nil
|
||||
}
|
||||
return "", errors.New("user not found")
|
||||
}
|
||||
|
||||
curried := Curry1(getUser)
|
||||
|
||||
rr1 := curried(42)
|
||||
v, err := rr1(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", v)
|
||||
|
||||
rr2 := curried(99)
|
||||
_, err = rr2(context.Background())
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestCurry2(t *testing.T) {
|
||||
queryDB := func(ctx context.Context, table string, id int) (string, error) {
|
||||
if table == "users" && id == 42 {
|
||||
return "record", nil
|
||||
}
|
||||
return "", errors.New("not found")
|
||||
}
|
||||
|
||||
curried := Curry2(queryDB)
|
||||
|
||||
rr1 := curried("users")(42)
|
||||
v, err := rr1(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "record", v)
|
||||
|
||||
rr2 := curried("posts")(1)
|
||||
_, err = rr2(context.Background())
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestCurry3(t *testing.T) {
|
||||
updateRecord := func(ctx context.Context, table string, id int, data string) (string, error) {
|
||||
if table == "users" && id == 42 && data == "updated" {
|
||||
return "success", nil
|
||||
}
|
||||
return "", errors.New("update failed")
|
||||
}
|
||||
|
||||
curried := Curry3(updateRecord)
|
||||
|
||||
rr1 := curried("users")(42)("updated")
|
||||
v, err := rr1(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "success", v)
|
||||
|
||||
rr2 := curried("posts")(1)("data")
|
||||
_, err = rr2(context.Background())
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestUncurry1(t *testing.T) {
|
||||
rrf := func(id int) ReaderResult[context.Context, string] {
|
||||
if id == 42 {
|
||||
return Of[context.Context]("Alice")
|
||||
}
|
||||
return Left[context.Context, string](errors.New("user not found"))
|
||||
}
|
||||
|
||||
gofunc := Uncurry1(rrf)
|
||||
|
||||
res1, err1 := gofunc(context.Background(), 42)
|
||||
assert.NoError(t, err1)
|
||||
assert.Equal(t, "Alice", res1)
|
||||
|
||||
res2, err2 := gofunc(context.Background(), 99)
|
||||
assert.Error(t, err2)
|
||||
assert.Equal(t, "", res2)
|
||||
}
|
||||
|
||||
func TestUncurry2(t *testing.T) {
|
||||
rrf := func(table string) func(int) ReaderResult[context.Context, string] {
|
||||
return func(id int) ReaderResult[context.Context, string] {
|
||||
if table == "users" && id == 42 {
|
||||
return Of[context.Context]("record")
|
||||
}
|
||||
return Left[context.Context, string](errors.New("not found"))
|
||||
}
|
||||
}
|
||||
|
||||
gofunc := Uncurry2(rrf)
|
||||
|
||||
res1, err1 := gofunc(context.Background(), "users", 42)
|
||||
assert.NoError(t, err1)
|
||||
assert.Equal(t, "record", res1)
|
||||
|
||||
res2, err2 := gofunc(context.Background(), "posts", 1)
|
||||
assert.Error(t, err2)
|
||||
assert.Equal(t, "", res2)
|
||||
}
|
||||
|
||||
func TestUncurry3(t *testing.T) {
|
||||
rrf := func(table string) func(int) func(string) ReaderResult[context.Context, string] {
|
||||
return func(id int) func(string) ReaderResult[context.Context, string] {
|
||||
return func(data string) ReaderResult[context.Context, string] {
|
||||
if table == "users" && id == 42 && data == "updated" {
|
||||
return Of[context.Context]("success")
|
||||
}
|
||||
return Left[context.Context, string](errors.New("update failed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gofunc := Uncurry3(rrf)
|
||||
|
||||
res1, err1 := gofunc(context.Background(), "users", 42, "updated")
|
||||
assert.NoError(t, err1)
|
||||
assert.Equal(t, "success", res1)
|
||||
|
||||
res2, err2 := gofunc(context.Background(), "posts", 1, "data")
|
||||
assert.Error(t, err2)
|
||||
assert.Equal(t, "", res2)
|
||||
}
|
||||
|
||||
// Test round-trip conversions
|
||||
func TestCurryUncurryRoundTrip(t *testing.T) {
|
||||
// Original Go function
|
||||
original := func(ctx context.Context, id int) (string, error) {
|
||||
if id == 42 {
|
||||
return "Alice", nil
|
||||
}
|
||||
return "", errors.New("not found")
|
||||
}
|
||||
|
||||
// Curry then uncurry
|
||||
curried := Curry1(original)
|
||||
uncurried := Uncurry1(curried)
|
||||
|
||||
// Should behave the same as original
|
||||
res1, err1 := original(context.Background(), 42)
|
||||
res2, err2 := uncurried(context.Background(), 42)
|
||||
assert.Equal(t, res1, res2)
|
||||
assert.Equal(t, err1, err2)
|
||||
|
||||
res3, err3 := original(context.Background(), 99)
|
||||
res4, err4 := uncurried(context.Background(), 99)
|
||||
assert.Equal(t, res3, res4)
|
||||
assert.Equal(t, err3, err4)
|
||||
}
|
||||
178
v2/idiomatic/readerresult/doc.go
Normal file
178
v2/idiomatic/readerresult/doc.go
Normal file
@@ -0,0 +1,178 @@
|
||||
// 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 provides a ReaderResult monad that combines the Reader and Result monads.
|
||||
//
|
||||
// A ReaderResult[R, A] represents a computation that:
|
||||
// - Depends on an environment of type R (Reader aspect)
|
||||
// - May fail with an error (Result aspect, which is Either[error, A])
|
||||
//
|
||||
// This is equivalent to Reader[R, Result[A]] or Reader[R, Either[error, A]].
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// ReaderResult is particularly useful for:
|
||||
//
|
||||
// 1. Dependency injection with error handling - pass configuration/services through
|
||||
// computations that may fail
|
||||
// 2. Functional error handling - compose operations that depend on context and may error
|
||||
// 3. Testing - easily mock dependencies by changing the environment value
|
||||
//
|
||||
// # Basic Example
|
||||
//
|
||||
// type Config struct {
|
||||
// DatabaseURL string
|
||||
// }
|
||||
//
|
||||
// // Function that needs config and may fail
|
||||
// func getUser(id int) readerresult.ReaderResult[Config, User] {
|
||||
// return readerresult.Asks(func(cfg Config) result.Result[User] {
|
||||
// // Use cfg.DatabaseURL to fetch user
|
||||
// return result.Of(user)
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// // Execute by providing the config
|
||||
// cfg := Config{DatabaseURL: "postgres://..."}
|
||||
// user, err := getUser(42)(cfg) // Returns (User, error)
|
||||
//
|
||||
// # Composition
|
||||
//
|
||||
// ReaderResult provides several ways to compose computations:
|
||||
//
|
||||
// 1. Map - transform successful values
|
||||
// 2. Chain (FlatMap) - sequence dependent operations
|
||||
// 3. Ap - combine independent computations
|
||||
// 4. Do-notation - imperative-style composition with Bind
|
||||
//
|
||||
// # Do-Notation Example
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Posts []Post
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readerresult.Do[Config](State{}),
|
||||
// readerresult.Bind(
|
||||
// func(user User) func(State) State {
|
||||
// return func(s State) State { s.User = user; return s }
|
||||
// },
|
||||
// func(s State) readerresult.ReaderResult[Config, User] {
|
||||
// return getUser(42)
|
||||
// },
|
||||
// ),
|
||||
// readerresult.Bind(
|
||||
// func(posts []Post) func(State) State {
|
||||
// return func(s State) State { s.Posts = posts; return s }
|
||||
// },
|
||||
// func(s State) readerresult.ReaderResult[Config, []Post] {
|
||||
// return getPosts(s.User.ID)
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
// # 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:
|
||||
//
|
||||
// - Left/Right - create failed/successful values
|
||||
// - GetOrElse - provide a default value for errors
|
||||
// - OrElse - recover from errors with an alternative computation
|
||||
// - Fold - handle both success and failure cases
|
||||
// - ChainEitherK - lift result.Result computations into ReaderResult
|
||||
//
|
||||
// # Relationship to Other Monads
|
||||
//
|
||||
// ReaderResult is related to several other monads in this library:
|
||||
//
|
||||
// - Reader[R, A] - ReaderResult without error handling
|
||||
// - Result[A] (Either[error, A]) - error handling without environment
|
||||
// - ReaderEither[R, E, A] - like ReaderResult but with custom error type E
|
||||
// - IOResult[A] - like ReaderResult but with no environment (IO with errors)
|
||||
//
|
||||
// # Performance Note
|
||||
//
|
||||
// ReaderResult is a zero-cost abstraction - it compiles to a simple function type
|
||||
// with no runtime overhead beyond the underlying computation.
|
||||
package readerresult
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user