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

Compare commits

...

23 Commits

Author SHA1 Message Date
Dr. Carsten Leue
62a3365b20 fix: add conversion prisms for numbers
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-27 13:12:18 +01:00
Dr. Carsten Leue
d9a16a6771 fix: add reduce operations to readerioresult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-26 17:00:10 +01:00
Dr. Carsten Leue
8949cc7dca fix: expose stats
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-26 13:44:40 +01:00
Dr. Carsten Leue
fa6b6caf22 fix: generic order for reader.Flap
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-26 12:53:13 +01:00
Dr. Carsten Leue
a1e8d397c3 fix: better doc and some helpers
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-26 12:06:09 +01:00
Dr. Carsten Leue
dbe7102e43 fix: better doc and some helpers
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-26 12:05:31 +01:00
Dr. Carsten Leue
09aeb996e2 fix: add GetOrElseOf
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-24 18:57:30 +01:00
Dr. Carsten Leue
7cd575d95a fix: improve Prism and Optional
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-24 18:22:52 +01:00
Dr. Carsten Leue
dcfb023891 fix: improve assertions
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-24 17:28:48 +01:00
Dr. Carsten Leue
51cf241a26 fix: add ReaderK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-24 12:29:55 +01:00
Dr. Carsten Leue
9004c93976 fix: add some idomatic helpers
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-24 10:40:58 +01:00
Dr. Carsten Leue
d8ab6b0ce5 fix: ChainReaderK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-22 10:39:56 +01:00
Dr. Carsten Leue
4e9998b645 fix: benchmarks and better docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-21 15:39:41 +01:00
Dr. Carsten Leue
2ea9e292e1 fix: idiomatic/readeresult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-21 15:25:59 +01:00
Dr. Carsten Leue
12a20e30d1 fix: implement BindReaderK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-21 13:01:27 +01:00
Dr. Carsten Leue
4909ad5473 fix: add missing monoid
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-21 10:22:50 +01:00
Dr. Carsten Leue
d116317cde fix: add readerresult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-21 10:04:28 +01:00
Dr. Carsten Leue
1428241f2c fix: race condition
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-21 08:36:07 +01:00
Dr. Carsten Leue
ef9216bad7 fix: documentation, tests, some utilities
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-20 08:43:15 +01:00
Dr. Carsten Leue
fe77c770b6 fix: cleanup types
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-19 17:36:49 +01:00
Dr. Carsten Leue
1c42b2ac1d fix: implement idiomatic/ioresult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-19 15:39:02 +01:00
Dr. Carsten Leue
cbd93fdecc fix: add statereaderioresult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-18 17:54:04 +01:00
Dr. Carsten Leue
6d94697128 fix: document statereaderioeither
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-18 16:06:56 +01:00
221 changed files with 29929 additions and 368 deletions

View File

@@ -3,7 +3,13 @@
"allow": [
"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 build:*)",
"Bash(go test:*)",
"Bash(go doc:*)",
"Bash(go tool cover:*)",
"Bash(sort:*)",
"Bash(tee:*)",
"Bash(find:*)"
],
"deny": [],
"ask": []

View File

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

View File

@@ -0,0 +1,174 @@
# Idiomatic ReadIOResult Functions - Implementation Plan
## Overview
This document outlines the idiomatic functions that should be added to the `readerioresult` package to support Go's native `(value, error)` pattern, similar to what was implemented for `readerresult`.
## Key Concepts
The idiomatic package `github.com/IBM/fp-go/v2/idiomatic/readerioresult` defines:
- `ReaderIOResult[R, A]` as `func(R) func() (A, error)` (idiomatic style)
- This contrasts with `readerioresult.ReaderIOResult[R, A]` which is `Reader[R, IOResult[A]]` (functional style)
## Functions to Add
### In `readerioresult/reader.go`
Add helper functions at the top:
```go
func fromReaderIOResultKleisliI[R, A, B any](f RIORI.Kleisli[R, A, B]) Kleisli[R, A, B] {
return function.Flow2(f, FromReaderIOResultI[R, B])
}
func fromIOResultKleisliI[A, B any](f IORI.Kleisli[A, B]) ioresult.Kleisli[A, B] {
return ioresult.Eitherize1(f)
}
```
### Core Conversion Functions
1. **FromResultI** - Lift `(value, error)` to ReaderIOResult
```go
func FromResultI[R, A any](a A, err error) ReaderIOResult[R, A]
```
2. **FromIOResultI** - Lift idiomatic IOResult to functional
```go
func FromIOResultI[R, A any](ioe func() (A, error)) ReaderIOResult[R, A]
```
3. **FromReaderIOResultI** - Convert idiomatic ReaderIOResult to functional
```go
func FromReaderIOResultI[R, A any](rr RIORI.ReaderIOResult[R, A]) ReaderIOResult[R, A]
```
### Chain Functions
4. **MonadChainI** / **ChainI** - Chain with idiomatic Kleisli
```go
func MonadChainI[R, A, B any](ma ReaderIOResult[R, A], f RIORI.Kleisli[R, A, B]) ReaderIOResult[R, B]
func ChainI[R, A, B any](f RIORI.Kleisli[R, A, B]) Operator[R, A, B]
```
5. **MonadChainEitherIK** / **ChainEitherIK** - Chain with idiomatic Result functions
```go
func MonadChainEitherIK[R, A, B any](ma ReaderIOResult[R, A], f func(A) (B, error)) ReaderIOResult[R, B]
func ChainEitherIK[R, A, B any](f func(A) (B, error)) Operator[R, A, B]
```
6. **MonadChainIOResultIK** / **ChainIOResultIK** - Chain with idiomatic IOResult
```go
func MonadChainIOResultIK[R, A, B any](ma ReaderIOResult[R, A], f func(A) func() (B, error)) ReaderIOResult[R, B]
func ChainIOResultIK[R, A, B any](f func(A) func() (B, error)) Operator[R, A, B]
```
### Applicative Functions
7. **MonadApI** / **ApI** - Apply with idiomatic value
```go
func MonadApI[B, R, A any](fab ReaderIOResult[R, func(A) B], fa RIORI.ReaderIOResult[R, A]) ReaderIOResult[R, B]
func ApI[B, R, A any](fa RIORI.ReaderIOResult[R, A]) Operator[R, func(A) B, B]
```
### Error Handling Functions
8. **OrElseI** - Fallback with idiomatic computation
```go
func OrElseI[R, A any](onLeft RIORI.Kleisli[R, error, A]) Operator[R, A, A]
```
9. **MonadAltI** / **AltI** - Alternative with idiomatic computation
```go
func MonadAltI[R, A any](first ReaderIOResult[R, A], second Lazy[RIORI.ReaderIOResult[R, A]]) ReaderIOResult[R, A]
func AltI[R, A any](second Lazy[RIORI.ReaderIOResult[R, A]]) Operator[R, A, A]
```
### Flatten Functions
10. **FlattenI** - Flatten nested idiomatic ReaderIOResult
```go
func FlattenI[R, A any](mma ReaderIOResult[R, RIORI.ReaderIOResult[R, A]]) ReaderIOResult[R, A]
```
### In `readerioresult/bind.go`
11. **BindI** - Bind with idiomatic Kleisli
```go
func BindI[R, S1, S2, T any](setter func(T) func(S1) S2, f RIORI.Kleisli[R, S1, T]) Operator[R, S1, S2]
```
12. **ApIS** - Apply idiomatic value to state
```go
func ApIS[R, S1, S2, T any](setter func(T) func(S1) S2, fa RIORI.ReaderIOResult[R, T]) Operator[R, S1, S2]
```
13. **ApISL** - Apply idiomatic value using lens
```go
func ApISL[R, S, T any](lens L.Lens[S, T], fa RIORI.ReaderIOResult[R, T]) Operator[R, S, S]
```
14. **BindIL** - Bind idiomatic with lens
```go
func BindIL[R, S, T any](lens L.Lens[S, T], f RIORI.Kleisli[R, T, T]) Operator[R, S, S]
```
15. **BindEitherIK** / **BindResultIK** - Bind idiomatic Result
```go
func BindEitherIK[R, S1, S2, T any](setter func(T) func(S1) S2, f func(S1) (T, error)) Operator[R, S1, S2]
func BindResultIK[R, S1, S2, T any](setter func(T) func(S1) S2, f func(S1) (T, error)) Operator[R, S1, S2]
```
16. **BindIOResultIK** - Bind idiomatic IOResult
```go
func BindIOResultIK[R, S1, S2, T any](setter func(T) func(S1) S2, f func(S1) func() (T, error)) Operator[R, S1, S2]
```
17. **BindToEitherI** / **BindToResultI** - Initialize from idiomatic pair
```go
func BindToEitherI[R, S1, T any](setter func(T) S1) func(T, error) ReaderIOResult[R, S1]
func BindToResultI[R, S1, T any](setter func(T) S1) func(T, error) ReaderIOResult[R, S1]
```
18. **BindToIOResultI** - Initialize from idiomatic IOResult
```go
func BindToIOResultI[R, S1, T any](setter func(T) S1) func(func() (T, error)) ReaderIOResult[R, S1]
```
19. **ApEitherIS** / **ApResultIS** - Apply idiomatic pair to state
```go
func ApEitherIS[R, S1, S2, T any](setter func(T) func(S1) S2) func(T, error) Operator[R, S1, S2]
func ApResultIS[R, S1, S2, T any](setter func(T) func(S1) S2) func(T, error) Operator[R, S1, S2]
```
20. **ApIOResultIS** - Apply idiomatic IOResult to state
```go
func ApIOResultIS[R, S1, S2, T any](setter func(T) func(S1) S2, fa func() (T, error)) Operator[R, S1, S2]
```
## Testing Strategy
Create `readerioresult/idiomatic_test.go` with:
- Tests for each idiomatic function
- Success and error cases
- Integration tests showing real-world usage patterns
- Parallel execution tests where applicable
- Complex scenarios combining multiple idiomatic functions
## Implementation Priority
1. **High Priority** - Core conversion and chain functions (1-6)
2. **Medium Priority** - Bind functions for do-notation (11-16)
3. **Low Priority** - Advanced applicative and error handling (7-10, 17-20)
## Benefits
1. **Seamless Integration** - Mix Go idiomatic code with functional pipelines
2. **Gradual Adoption** - Convert code incrementally from idiomatic to functional
3. **Interoperability** - Work with existing Go libraries that return `(value, error)`
4. **Consistency** - Mirrors the successful pattern from `readerresult`
## References
- See `readerresult` package for similar implementations
- See `idiomatic/readerresult` for the idiomatic types
- See `idiomatic/ioresult` for IO-level idiomatic patterns

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,209 @@
// 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 statereaderioresult
import (
"github.com/IBM/fp-go/v2/function"
A "github.com/IBM/fp-go/v2/internal/apply"
C "github.com/IBM/fp-go/v2/internal/chain"
F "github.com/IBM/fp-go/v2/internal/functor"
)
// Do starts a do-notation chain for building computations in a fluent style.
// This is typically used with Bind, Let, and other combinators to compose
// stateful, context-dependent computations that can fail.
//
// Example:
//
// type State struct {
// name string
// age int
// }
// result := function.Pipe2(
// statereaderioresult.Do[AppState](State{}),
// statereaderioresult.Bind(...),
// statereaderioresult.Let(...),
// )
//
//go:inline
func Do[ST, A any](
empty A,
) StateReaderIOResult[ST, A] {
return Of[ST](empty)
}
// Bind executes a computation and binds its result to a field in the accumulator state.
// This is used in do-notation to sequence dependent computations.
//
// Example:
//
// result := function.Pipe2(
// statereaderioresult.Do[AppState](State{}),
// statereaderioresult.Bind(
// func(name string) func(State) State {
// return func(s State) State { return State{name: name, age: s.age} }
// },
// func(s State) statereaderioresult.StateReaderIOResult[AppState, string] {
// return statereaderioresult.Of[AppState]("John")
// },
// ),
// )
//
//go:inline
func Bind[ST, S1, S2, T any](
setter func(T) func(S1) S2,
f Kleisli[ST, S1, T],
) Operator[ST, S1, S2] {
return C.Bind(
Chain[ST, S1, S2],
Map[ST, T, S2],
setter,
f,
)
}
// Let computes a derived value and binds it to a field in the accumulator state.
// Unlike Bind, this does not execute a monadic computation, just a pure function.
//
// Example:
//
// result := function.Pipe2(
// statereaderioresult.Do[AppState](State{age: 25}),
// statereaderioresult.Let(
// func(isAdult bool) func(State) State {
// return func(s State) State { return State{age: s.age, isAdult: isAdult} }
// },
// func(s State) bool { return s.age >= 18 },
// ),
// )
//
//go:inline
func Let[ST, S1, S2, T any](
key func(T) func(S1) S2,
f func(S1) T,
) Operator[ST, S1, S2] {
return F.Let(
Map[ST, S1, S2],
key,
f,
)
}
// LetTo binds a constant value to a field in the accumulator state.
//
// Example:
//
// result := function.Pipe2(
// statereaderioresult.Do[AppState](State{}),
// statereaderioresult.LetTo(
// func(status string) func(State) State {
// return func(s State) State { return State{...s, status: status} }
// },
// "active",
// ),
// )
//
//go:inline
func LetTo[ST, S1, S2, T any](
key func(T) func(S1) S2,
b T,
) Operator[ST, S1, S2] {
return F.LetTo(
Map[ST, S1, S2],
key,
b,
)
}
// BindTo wraps a value in a simple constructor, typically used to start a do-notation chain
// after getting an initial value.
//
// Example:
//
// result := function.Pipe2(
// statereaderioresult.Of[AppState](42),
// statereaderioresult.BindTo[AppState](func(x int) State { return State{value: x} }),
// )
//
//go:inline
func BindTo[ST, S1, T any](
setter func(T) S1,
) Operator[ST, T, S1] {
return C.BindTo(
Map[ST, T, S1],
setter,
)
}
// ApS applies a computation in sequence and binds the result to a field.
// This is the applicative version of Bind.
//
//go:inline
func ApS[ST, S1, S2, T any](
setter func(T) func(S1) S2,
fa StateReaderIOResult[ST, T],
) Operator[ST, S1, S2] {
return A.ApS(
Ap[S2, ST, T],
Map[ST, S1, func(T) S2],
setter,
fa,
)
}
// ApSL is a lens-based variant of ApS for working with nested structures.
// It uses a lens to focus on a specific field in the state.
//
//go:inline
func ApSL[ST, S, T any](
lens Lens[S, T],
fa StateReaderIOResult[ST, T],
) Endomorphism[StateReaderIOResult[ST, S]] {
return ApS(lens.Set, fa)
}
// BindL is a lens-based variant of Bind for working with nested structures.
// It uses a lens to focus on a specific field in the state.
//
//go:inline
func BindL[ST, S, T any](
lens Lens[S, T],
f Kleisli[ST, T, T],
) Endomorphism[StateReaderIOResult[ST, S]] {
return Bind(lens.Set, function.Flow2(lens.Get, f))
}
// LetL is a lens-based variant of Let for working with nested structures.
// It uses a lens to focus on a specific field in the state.
//
//go:inline
func LetL[ST, S, T any](
lens Lens[S, T],
f Endomorphism[T],
) Endomorphism[StateReaderIOResult[ST, S]] {
return Let[ST](lens.Set, function.Flow2(lens.Get, f))
}
// LetToL is a lens-based variant of LetTo for working with nested structures.
// It uses a lens to focus on a specific field in the state.
//
//go:inline
func LetToL[ST, S, T any](
lens Lens[S, T],
b T,
) Endomorphism[StateReaderIOResult[ST, S]] {
return LetTo[ST](lens.Set, b)
}

View File

@@ -0,0 +1,147 @@
// 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 statereaderioresult provides a functional programming abstraction that combines
// four powerful concepts: State, Reader, IO, and Result monads, specialized for Go's context.Context.
//
// # StateReaderIOResult
//
// StateReaderIOResult[S, A] represents a computation that:
// - Manages state of type S (State)
// - Depends on a [context.Context] (Reader)
// - Performs side effects (IO)
// - Can fail with an [error] or succeed with a value of type A (Result)
//
// This is a specialization of StateReaderIOEither with:
// - Context type fixed to [context.Context]
// - Error type fixed to [error]
//
// This is particularly useful for:
// - Stateful computations with dependency injection using Go contexts
// - Error handling in effectful computations with state
// - Composing operations that need access to context, manage state, and can fail
// - Working with Go's standard context patterns (cancellation, deadlines, values)
//
// # Core Operations
//
// Construction:
// - Of/Right: Create a successful computation with a value
// - Left: Create a failed computation with an error
// - FromState: Lift a State into StateReaderIOResult
// - FromIO: Lift an IO into StateReaderIOResult
// - FromResult: Lift a Result into StateReaderIOResult
// - FromIOResult: Lift an IOResult into StateReaderIOResult
// - FromReaderIOResult: Lift a ReaderIOResult into StateReaderIOResult
//
// Transformation:
// - Map: Transform the success value
// - Chain: Sequence dependent computations (monadic bind)
// - Flatten: Flatten nested StateReaderIOResult
//
// Combination:
// - Ap: Apply a function in a context to a value in a context
//
// Context Access:
// - Asks: Get a value derived from the context
// - Local: Run a computation with a modified context
//
// Kleisli Arrows:
// - FromResultK: Lift a Result-returning function to a Kleisli arrow
// - FromIOK: Lift an IO-returning function to a Kleisli arrow
// - FromIOResultK: Lift an IOResult-returning function to a Kleisli arrow
// - FromReaderIOResultK: Lift a ReaderIOResult-returning function to a Kleisli arrow
// - ChainResultK: Chain with a Result-returning function
// - ChainIOResultK: Chain with an IOResult-returning function
// - ChainReaderIOResultK: Chain with a ReaderIOResult-returning function
//
// Do Notation (Monadic Composition):
// - Do: Start a do-notation chain
// - Bind: Bind a value from a computation
// - BindTo: Bind a value to a simple constructor
// - Let: Compute a derived value
// - LetTo: Set a constant value
// - ApS: Apply in sequence (for applicative composition)
// - BindL/ApSL/LetL/LetToL: Lens-based variants for working with nested structures
//
// # Example Usage
//
// type AppState struct {
// RequestCount int
// LastError error
// }
//
// // A computation that manages state, depends on context, performs IO, and can fail
// func processRequest(data string) statereaderioresult.StateReaderIOResult[AppState, string] {
// return func(state AppState) readerioresult.ReaderIOResult[pair.Pair[AppState, string]] {
// return func(ctx context.Context) ioresult.IOResult[pair.Pair[AppState, string]] {
// return func() result.Result[pair.Pair[AppState, string]] {
// // Check context for cancellation
// if ctx.Err() != nil {
// return result.Error[pair.Pair[AppState, string]](ctx.Err())
// }
// // Update state
// newState := AppState{RequestCount: state.RequestCount + 1}
// // Perform IO operations
// return result.Of(pair.MakePair(newState, "processed: " + data))
// }
// }
// }
// }
//
// // Compose operations using do-notation
// result := function.Pipe3(
// statereaderioresult.Do[AppState](State{}),
// statereaderioresult.Bind(
// func(result string) func(State) State { return func(s State) State { return State{result: result} } },
// func(s State) statereaderioresult.StateReaderIOResult[AppState, string] {
// return processRequest(s.input)
// },
// ),
// statereaderioresult.Map[AppState](func(s State) string { return s.result }),
// )
//
// // Execute with initial state and context
// initialState := AppState{RequestCount: 0}
// ctx := context.Background()
// outcome := result(initialState)(ctx)() // Returns result.Result[pair.Pair[AppState, string]]
//
// # Context Integration
//
// This package is designed to work seamlessly with Go's context.Context:
//
// // Using context values
// getUserID := statereaderioresult.Asks[AppState, string](func(ctx context.Context) statereaderioresult.StateReaderIOResult[AppState, string] {
// userID, ok := ctx.Value("userID").(string)
// if !ok {
// return statereaderioresult.Left[AppState, string](errors.New("missing userID"))
// }
// return statereaderioresult.Of[AppState](userID)
// })
//
// // Using context cancellation
// withTimeout := statereaderioresult.Local[AppState, string](func(ctx context.Context) context.Context {
// ctx, _ = context.WithTimeout(ctx, 5*time.Second)
// return ctx
// })
//
// # Monad Laws
//
// StateReaderIOResult satisfies the monad laws:
// - Left Identity: Of(a) >>= f ≡ f(a)
// - Right Identity: m >>= Of ≡ m
// - Associativity: (m >>= f) >>= g ≡ m >>= (x => f(x) >>= g)
//
// These laws are verified in the testing subpackage.
package statereaderioresult

View File

@@ -0,0 +1,41 @@
// 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 statereaderioresult
import (
"context"
"github.com/IBM/fp-go/v2/eq"
"github.com/IBM/fp-go/v2/function"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
)
// Eq implements the equals predicate for values contained in the [StateReaderIOResult] monad
func Eq[S, A any](eqr eq.Eq[ReaderIOResult[Pair[S, A]]]) func(S) eq.Eq[StateReaderIOResult[S, A]] {
return func(s S) eq.Eq[StateReaderIOResult[S, A]] {
return eq.FromEquals(func(l, r StateReaderIOResult[S, A]) bool {
return eqr.Equals(l(s), r(s))
})
}
}
// FromStrictEquals constructs an [eq.Eq] from the canonical comparison function
func FromStrictEquals[S comparable, A comparable]() func(context.Context) func(S) eq.Eq[StateReaderIOResult[S, A]] {
return function.Flow2(
RIOR.FromStrictEquals[context.Context, Pair[S, A]](),
Eq[S, A],
)
}

View File

@@ -0,0 +1,103 @@
// 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 statereaderioresult
import (
"github.com/IBM/fp-go/v2/internal/applicative"
"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 stateReaderIOResultPointed[
S, A any,
] struct{}
type stateReaderIOResultFunctor[
S, A, B any,
] struct{}
type stateReaderIOResultApplicative[
S, A, B any,
] struct{}
type stateReaderIOResultMonad[
S, A, B any,
] struct{}
func (o *stateReaderIOResultPointed[S, A]) Of(a A) StateReaderIOResult[S, A] {
return Of[S](a)
}
func (o *stateReaderIOResultMonad[S, A, B]) Of(a A) StateReaderIOResult[S, A] {
return Of[S](a)
}
func (o *stateReaderIOResultApplicative[S, A, B]) Of(a A) StateReaderIOResult[S, A] {
return Of[S](a)
}
func (o *stateReaderIOResultMonad[S, A, B]) Map(f func(A) B) Operator[S, A, B] {
return Map[S](f)
}
func (o *stateReaderIOResultApplicative[S, A, B]) Map(f func(A) B) Operator[S, A, B] {
return Map[S](f)
}
func (o *stateReaderIOResultFunctor[S, A, B]) Map(f func(A) B) Operator[S, A, B] {
return Map[S](f)
}
func (o *stateReaderIOResultMonad[S, A, B]) Chain(f Kleisli[S, A, B]) Operator[S, A, B] {
return Chain(f)
}
func (o *stateReaderIOResultMonad[S, A, B]) Ap(fa StateReaderIOResult[S, A]) Operator[S, func(A) B, B] {
return Ap[B](fa)
}
func (o *stateReaderIOResultApplicative[S, A, B]) Ap(fa StateReaderIOResult[S, A]) Operator[S, func(A) B, B] {
return Ap[B](fa)
}
// Pointed implements the [pointed.Pointed] operations for [StateReaderIOResult]
func Pointed[
S, A any,
]() pointed.Pointed[A, StateReaderIOResult[S, A]] {
return &stateReaderIOResultPointed[S, A]{}
}
// Functor implements the [functor.Functor] operations for [StateReaderIOResult]
func Functor[
S, A, B any,
]() functor.Functor[A, B, StateReaderIOResult[S, A], StateReaderIOResult[S, B]] {
return &stateReaderIOResultFunctor[S, A, B]{}
}
// Applicative implements the [applicative.Applicative] operations for [StateReaderIOResult]
func Applicative[
S, A, B any,
]() applicative.Applicative[A, B, StateReaderIOResult[S, A], StateReaderIOResult[S, B], StateReaderIOResult[S, func(A) B]] {
return &stateReaderIOResultApplicative[S, A, B]{}
}
// Monad implements the [monad.Monad] operations for [StateReaderIOResult]
func Monad[
S, A, B any,
]() monad.Monad[A, B, StateReaderIOResult[S, A], StateReaderIOResult[S, B], StateReaderIOResult[S, func(A) B]] {
return &stateReaderIOResultMonad[S, A, B]{}
}

View File

@@ -0,0 +1,101 @@
// 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 statereaderioresult
import "github.com/IBM/fp-go/v2/statereaderioeither"
// WithResource constructs a function that creates a resource with state management, operates on it,
// and then releases the resource. This ensures proper resource cleanup even in the presence of errors,
// following the Resource Acquisition Is Initialization (RAII) pattern.
//
// The state is threaded through all operations: resource creation, usage, and release.
//
// The resource lifecycle with state management is:
// 1. onCreate: Acquires the resource (may modify state)
// 2. use: Operates on the resource with current state (provided as argument to the returned function)
// 3. onRelease: Releases the resource with current state (called regardless of success or failure)
//
// Type parameters:
// - A: The type of the result produced by using the resource
// - S: The state type that is threaded through all operations
// - RES: The resource type
// - ANY: The type returned by the release function (typically ignored)
//
// Parameters:
// - onCreate: A stateful computation that acquires the resource
// - onRelease: A stateful function that releases the resource, called with the resource and current state,
// executed regardless of errors
//
// Returns:
//
// A function that takes a resource-using function and returns a StateReaderIOResult that manages
// the resource lifecycle with state
//
// Example:
//
// type AppState struct {
// openFiles int
// }
//
// // Resource creation that updates state
// openFile := func(filename string) StateReaderIOResult[AppState, *File] {
// return func(state AppState) ReaderIOResult[Pair[AppState, *File]] {
// return func(ctx context.Context) IOResult[Pair[AppState, *File]] {
// return func() Result[Pair[AppState, *File]] {
// file, err := os.Open(filename)
// if err != nil {
// return result.Error[Pair[AppState, *File]](err)
// }
// newState := AppState{openFiles: state.openFiles + 1}
// return result.Of(pair.MakePair(newState, file))
// }
// }
// }
// }
//
// // Resource release that updates state
// closeFile := func(f *File) StateReaderIOResult[AppState, int] {
// return func(state AppState) ReaderIOResult[Pair[AppState, int]] {
// return func(ctx context.Context) IOResult[Pair[AppState, int]] {
// return func() Result[Pair[AppState, int]] {
// f.Close()
// newState := AppState{openFiles: state.openFiles - 1}
// return result.Of(pair.MakePair(newState, 0))
// }
// }
// }
// }
//
// // Use the resource with automatic cleanup
// withFile := WithResource(
// openFile("data.txt"),
// closeFile,
// )
//
// result := withFile(func(f *File) StateReaderIOResult[AppState, string] {
// return readContent(f) // File will be closed automatically
// })
//
// // Execute the computation
// initialState := AppState{openFiles: 0}
// ctx := context.Background()
// outcome := result(initialState)(ctx)()
func WithResource[A, S, RES, ANY any](
onCreate StateReaderIOResult[S, RES],
onRelease Kleisli[S, RES, ANY],
) Kleisli[S, Kleisli[S, RES, A], A] {
return statereaderioeither.WithResource[A](onCreate, onRelease)
}

View File

@@ -0,0 +1,415 @@
// 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 statereaderioresult
import (
"context"
"errors"
"testing"
P "github.com/IBM/fp-go/v2/pair"
RES "github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
// resourceState tracks the lifecycle of resources for testing
type resourceState struct {
resourcesCreated int
resourcesReleased int
lastError error
}
// mockResource represents a test resource
type mockResource struct {
id int
isValid bool
}
// TestWithResourceSuccess tests successful resource creation, usage, and release
func TestWithResourceSuccess(t *testing.T) {
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
ctx := context.Background()
// Create a resource
onCreate := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] {
return func(ctx context.Context) IOResult[Pair[resourceState, mockResource]] {
return func() Result[Pair[resourceState, mockResource]] {
newState := resourceState{
resourcesCreated: s.resourcesCreated + 1,
resourcesReleased: s.resourcesReleased,
}
res := mockResource{id: newState.resourcesCreated, isValid: true}
return RES.Of(P.MakePair(newState, res))
}
}
}
// Release a resource
onRelease := func(res mockResource) StateReaderIOResult[resourceState, int] {
return func(s resourceState) ReaderIOResult[Pair[resourceState, int]] {
return func(ctx context.Context) IOResult[Pair[resourceState, int]] {
return func() Result[Pair[resourceState, int]] {
newState := resourceState{
resourcesCreated: s.resourcesCreated,
resourcesReleased: s.resourcesReleased + 1,
}
return RES.Of(P.MakePair(newState, 0))
}
}
}
}
// Use the resource
useResource := func(res mockResource) StateReaderIOResult[resourceState, string] {
return func(s resourceState) ReaderIOResult[Pair[resourceState, string]] {
return func(ctx context.Context) IOResult[Pair[resourceState, string]] {
return func() Result[Pair[resourceState, string]] {
result := "used resource " + string(rune(res.id+'0'))
return RES.Of(P.MakePair(s, result))
}
}
}
}
withResource := WithResource[string](onCreate, onRelease)
result := withResource(useResource)
outcome := result(initialState)(ctx)()
assert.True(t, RES.IsRight(outcome))
RES.Map(func(p Pair[resourceState, string]) Pair[resourceState, string] {
state := P.Head(p)
value := P.Tail(p)
// Verify state updates
// Note: Final state comes from the use function, not the release function
// onCreate: 0->1, use: sees 1 (doesn't modify), release: sees 1 and increments released
// The final state is from use function which saw state=1 with resourcesReleased=0
assert.Equal(t, 1, state.resourcesCreated, "Resource should be created once")
assert.Equal(t, 0, state.resourcesReleased, "Final state is from use function, before release")
// Verify result
assert.Equal(t, "used resource 1", value)
return p
})(outcome)
}
// TestWithResourceErrorInCreate tests error handling when resource creation fails
func TestWithResourceErrorInCreate(t *testing.T) {
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
ctx := context.Background()
createError := errors.New("failed to create resource")
// onCreate that fails
onCreate := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] {
return func(ctx context.Context) IOResult[Pair[resourceState, mockResource]] {
return func() Result[Pair[resourceState, mockResource]] {
return RES.Left[Pair[resourceState, mockResource]](createError)
}
}
}
// Release should not be called if onCreate fails
onRelease := func(res mockResource) StateReaderIOResult[resourceState, int] {
return func(s resourceState) ReaderIOResult[Pair[resourceState, int]] {
return func(ctx context.Context) IOResult[Pair[resourceState, int]] {
return func() Result[Pair[resourceState, int]] {
t.Error("onRelease should not be called when onCreate fails")
return RES.Of(P.MakePair(s, 0))
}
}
}
}
useResource := func(res mockResource) StateReaderIOResult[resourceState, string] {
return Of[resourceState]("should not reach here")
}
withResource := WithResource[string](onCreate, onRelease)
result := withResource(useResource)
outcome := result(initialState)(ctx)()
assert.True(t, RES.IsLeft(outcome))
RES.Fold(
func(err error) bool {
assert.Equal(t, createError, err)
return true
},
func(p Pair[resourceState, string]) bool {
t.Error("Expected error but got success")
return false
},
)(outcome)
}
// TestWithResourceErrorInUse tests that resources are released even when usage fails
func TestWithResourceErrorInUse(t *testing.T) {
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
ctx := context.Background()
useError := errors.New("failed to use resource")
// Create a resource
onCreate := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] {
return func(ctx context.Context) IOResult[Pair[resourceState, mockResource]] {
return func() Result[Pair[resourceState, mockResource]] {
newState := resourceState{
resourcesCreated: s.resourcesCreated + 1,
resourcesReleased: s.resourcesReleased,
}
res := mockResource{id: 1, isValid: true}
return RES.Of(P.MakePair(newState, res))
}
}
}
releaseWasCalled := false
// Release should still be called even if use fails
onRelease := func(res mockResource) StateReaderIOResult[resourceState, int] {
return func(s resourceState) ReaderIOResult[Pair[resourceState, int]] {
return func(ctx context.Context) IOResult[Pair[resourceState, int]] {
return func() Result[Pair[resourceState, int]] {
releaseWasCalled = true
newState := resourceState{
resourcesCreated: s.resourcesCreated,
resourcesReleased: s.resourcesReleased + 1,
}
return RES.Of(P.MakePair(newState, 0))
}
}
}
}
// Use that fails
useResource := func(res mockResource) StateReaderIOResult[resourceState, string] {
return Left[resourceState, string](useError)
}
withResource := WithResource[string](onCreate, onRelease)
result := withResource(useResource)
outcome := result(initialState)(ctx)()
assert.True(t, RES.IsLeft(outcome))
assert.True(t, releaseWasCalled, "onRelease should be called even when use fails")
RES.Fold(
func(err error) bool {
assert.Equal(t, useError, err)
return true
},
func(p Pair[resourceState, string]) bool {
t.Error("Expected error but got success")
return false
},
)(outcome)
}
// TestWithResourceStateThreading tests that state is properly threaded through all operations
func TestWithResourceStateThreading(t *testing.T) {
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
ctx := context.Background()
// Create increments counter
onCreate := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] {
return func(ctx context.Context) IOResult[Pair[resourceState, mockResource]] {
return func() Result[Pair[resourceState, mockResource]] {
newState := resourceState{
resourcesCreated: s.resourcesCreated + 1,
resourcesReleased: s.resourcesReleased,
}
res := mockResource{id: newState.resourcesCreated, isValid: true}
return RES.Of(P.MakePair(newState, res))
}
}
}
// Use observes the state after creation
useResource := func(res mockResource) StateReaderIOResult[resourceState, int] {
return func(s resourceState) ReaderIOResult[Pair[resourceState, int]] {
return func(ctx context.Context) IOResult[Pair[resourceState, int]] {
return func() Result[Pair[resourceState, int]] {
// Verify state was updated by onCreate
assert.Equal(t, 1, s.resourcesCreated)
assert.Equal(t, 0, s.resourcesReleased)
return RES.Of(P.MakePair(s, s.resourcesCreated))
}
}
}
}
// Release increments released counter
onRelease := func(res mockResource) StateReaderIOResult[resourceState, int] {
return func(s resourceState) ReaderIOResult[Pair[resourceState, int]] {
return func(ctx context.Context) IOResult[Pair[resourceState, int]] {
return func() Result[Pair[resourceState, int]] {
// Verify state was updated by onCreate and use
assert.Equal(t, 1, s.resourcesCreated)
assert.Equal(t, 0, s.resourcesReleased)
newState := resourceState{
resourcesCreated: s.resourcesCreated,
resourcesReleased: s.resourcesReleased + 1,
}
return RES.Of(P.MakePair(newState, 0))
}
}
}
}
withResource := WithResource[int](onCreate, onRelease)
result := withResource(useResource)
outcome := result(initialState)(ctx)()
assert.True(t, RES.IsRight(outcome))
RES.Map(func(p Pair[resourceState, int]) Pair[resourceState, int] {
finalState := P.Head(p)
value := P.Tail(p)
// Verify final state
// Note: Final state is from the use function, which preserves the state it received
// onCreate: 0->1, use: sees 1, release: sees 1 and increments released to 1
// But final state is from use function where resourcesReleased=0
assert.Equal(t, 1, finalState.resourcesCreated)
assert.Equal(t, 0, finalState.resourcesReleased, "Final state is from use function, before release")
assert.Equal(t, 1, value)
return p
})(outcome)
}
// TestWithResourceMultipleResources tests using WithResource multiple times (nesting)
func TestWithResourceMultipleResources(t *testing.T) {
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
ctx := context.Background()
createResource := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] {
return func(ctx context.Context) IOResult[Pair[resourceState, mockResource]] {
return func() Result[Pair[resourceState, mockResource]] {
newState := resourceState{
resourcesCreated: s.resourcesCreated + 1,
resourcesReleased: s.resourcesReleased,
}
res := mockResource{id: newState.resourcesCreated, isValid: true}
return RES.Of(P.MakePair(newState, res))
}
}
}
releaseResource := func(res mockResource) StateReaderIOResult[resourceState, int] {
return func(s resourceState) ReaderIOResult[Pair[resourceState, int]] {
return func(ctx context.Context) IOResult[Pair[resourceState, int]] {
return func() Result[Pair[resourceState, int]] {
newState := resourceState{
resourcesCreated: s.resourcesCreated,
resourcesReleased: s.resourcesReleased + 1,
}
return RES.Of(P.MakePair(newState, 0))
}
}
}
}
// Create two nested resources
withResource1 := WithResource[int](createResource, releaseResource)
withResource2 := WithResource[int](createResource, releaseResource)
result := withResource1(func(res1 mockResource) StateReaderIOResult[resourceState, int] {
return withResource2(func(res2 mockResource) StateReaderIOResult[resourceState, int] {
// Both resources should be available
return Of[resourceState](res1.id + res2.id)
})
})
outcome := result(initialState)(ctx)()
assert.True(t, RES.IsRight(outcome))
RES.Map(func(p Pair[resourceState, int]) Pair[resourceState, int] {
finalState := P.Head(p)
value := P.Tail(p)
// Both resources created, but final state is from innermost use function
// onCreate1: 0->1, onCreate2: 1->2, use (Of): sees 2
// Release functions execute but their state changes aren't in the final result
assert.Equal(t, 2, finalState.resourcesCreated)
assert.Equal(t, 0, finalState.resourcesReleased, "Final state is from use function, before releases")
// res1.id = 1, res2.id = 2, sum = 3
assert.Equal(t, 3, value)
return p
})(outcome)
}
// TestWithResourceContextCancellation tests behavior with context cancellation
func TestWithResourceContextCancellation(t *testing.T) {
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
cancelError := errors.New("context cancelled")
// Create should respect context cancellation
onCreate := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] {
return func(ctx context.Context) IOResult[Pair[resourceState, mockResource]] {
return func() Result[Pair[resourceState, mockResource]] {
if ctx.Err() != nil {
return RES.Left[Pair[resourceState, mockResource]](cancelError)
}
newState := resourceState{
resourcesCreated: s.resourcesCreated + 1,
resourcesReleased: s.resourcesReleased,
}
res := mockResource{id: 1, isValid: true}
return RES.Of(P.MakePair(newState, res))
}
}
}
onRelease := func(res mockResource) StateReaderIOResult[resourceState, int] {
return func(s resourceState) ReaderIOResult[Pair[resourceState, int]] {
return func(ctx context.Context) IOResult[Pair[resourceState, int]] {
return func() Result[Pair[resourceState, int]] {
newState := resourceState{
resourcesCreated: s.resourcesCreated,
resourcesReleased: s.resourcesReleased + 1,
}
return RES.Of(P.MakePair(newState, 0))
}
}
}
}
useResource := func(res mockResource) StateReaderIOResult[resourceState, string] {
return Of[resourceState]("should not reach here")
}
withResource := WithResource[string](onCreate, onRelease)
result := withResource(useResource)
outcome := result(initialState)(ctx)()
assert.True(t, RES.IsLeft(outcome))
RES.Fold(
func(err error) bool {
assert.Equal(t, cancelError, err)
return true
},
func(p Pair[resourceState, string]) bool {
t.Error("Expected error but got success")
return false
},
)(outcome)
}

View File

@@ -0,0 +1,309 @@
// 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 statereaderioresult
import (
"context"
RIORES "github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/statet"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/result"
)
// Left creates a StateReaderIOResult that represents a failed computation with the given error.
// The error value is immediately available and does not depend on state or context.
//
// Example:
//
// result := statereaderioresult.Left[AppState, string](errors.New("validation failed"))
// // Returns a failed computation that ignores state and context
func Left[S, A any](e error) StateReaderIOResult[S, A] {
return function.Constant1[S](RIORES.Left[Pair[S, A]](e))
}
// Right creates a StateReaderIOResult that represents a successful computation with the given value.
// The value is wrapped and the state is passed through unchanged.
//
// Example:
//
// result := statereaderioresult.Right[AppState](42)
// // Returns a successful computation containing 42
func Right[S, A any](a A) StateReaderIOResult[S, A] {
return statet.Of[StateReaderIOResult[S, A]](RIORES.Of[Pair[S, A]], a)
}
// Of creates a StateReaderIOResult that represents a successful computation with the given value.
// This is the monadic return/pure operation for StateReaderIOResult.
// Equivalent to [Right].
//
// Example:
//
// result := statereaderioresult.Of[AppState](42)
// // Returns a successful computation containing 42
func Of[S, A any](a A) StateReaderIOResult[S, A] {
return Right[S](a)
}
// MonadMap transforms the success value of a StateReaderIOResult using the provided function.
// If the computation fails, the error is propagated unchanged.
// The state is threaded through the computation.
// This is the functor map operation.
//
// Example:
//
// result := statereaderioresult.MonadMap(
// statereaderioresult.Of[AppState](21),
// 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]](
RIORES.MonadMap[Pair[S, A], Pair[S, B]],
fa,
f,
)
}
// Map is the curried version of [MonadMap].
// Returns a function that transforms a StateReaderIOResult.
//
// Example:
//
// 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]](
RIORES.Map[Pair[S, A], Pair[S, B]],
f,
)
}
// MonadChain sequences two computations, passing the result of the first to a function
// that produces the second computation. This is the monadic bind operation.
// The state is threaded through both computations.
//
// Example:
//
// result := statereaderioresult.MonadChain(
// statereaderioresult.Of[AppState](5),
// func(x int) statereaderioresult.StateReaderIOResult[AppState, string] {
// return statereaderioresult.Of[AppState](fmt.Sprintf("value: %d", x))
// },
// )
func MonadChain[S, A, B any](fa StateReaderIOResult[S, A], f Kleisli[S, A, B]) StateReaderIOResult[S, B] {
return statet.MonadChain(
RIORES.MonadChain[Pair[S, A], Pair[S, B]],
fa,
f,
)
}
// Chain is the curried version of [MonadChain].
// Returns a function that sequences computations.
//
// Example:
//
// stringify := statereaderioresult.Chain[AppState](func(x int) statereaderioresult.StateReaderIOResult[AppState, string] {
// return statereaderioresult.Of[AppState](fmt.Sprintf("%d", x))
// })
// result := function.Pipe1(statereaderioresult.Of[AppState](42), stringify)
func Chain[S, A, B any](f Kleisli[S, A, B]) Operator[S, A, B] {
return statet.Chain[StateReaderIOResult[S, A]](
RIORES.Chain[Pair[S, A], Pair[S, B]],
f,
)
}
// MonadAp applies a function wrapped in a StateReaderIOResult to a value wrapped in a StateReaderIOResult.
// If either the function or the value fails, the error is propagated.
// The state is threaded through both computations sequentially.
// This is the applicative apply operation.
//
// Example:
//
// 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] {
return statet.MonadAp[StateReaderIOResult[S, A], StateReaderIOResult[S, B]](
RIORES.MonadMap[Pair[S, A], Pair[S, B]],
RIORES.MonadChain[Pair[S, func(A) B], Pair[S, B]],
fab,
fa,
)
}
// Ap is the curried version of [MonadAp].
// Returns a function that applies a wrapped function to the given wrapped value.
func Ap[B, S, A any](fa StateReaderIOResult[S, A]) Operator[S, func(A) B, B] {
return statet.Ap[StateReaderIOResult[S, A], StateReaderIOResult[S, B], StateReaderIOResult[S, func(A) B]](
RIORES.Map[Pair[S, A], Pair[S, B]],
RIORES.Chain[Pair[S, func(A) B], Pair[S, B]],
fa,
)
}
// FromReaderIOResult lifts a ReaderIOResult into a StateReaderIOResult.
// The state is passed through unchanged.
//
// Example:
//
// riores := readerioresult.Of(42)
// result := statereaderioresult.FromReaderIOResult[AppState](riores)
func FromReaderIOResult[S, A any](fa ReaderIOResult[A]) StateReaderIOResult[S, A] {
return statet.FromF[StateReaderIOResult[S, A]](
RIORES.MonadMap[A],
fa,
)
}
// FromIOResult lifts an IOResult into a StateReaderIOResult.
// The state is passed through unchanged and the context is ignored.
func FromIOResult[S, A any](fa IOResult[A]) StateReaderIOResult[S, A] {
return FromReaderIOResult[S](RIORES.FromIOResult(fa))
}
// FromState lifts a State computation into a StateReaderIOResult.
// The computation cannot fail (uses the error type).
func FromState[S, A any](sa State[S, A]) StateReaderIOResult[S, A] {
return statet.FromState[StateReaderIOResult[S, A]](RIORES.Of[Pair[S, A]], sa)
}
// FromIO lifts an IO computation into a StateReaderIOResult.
// The state is passed through unchanged and the context is ignored.
func FromIO[S, A any](fa IO[A]) StateReaderIOResult[S, A] {
return FromReaderIOResult[S](RIORES.FromIO(fa))
}
// FromResult lifts a Result into a StateReaderIOResult.
// The state is passed through unchanged and the context is ignored.
//
// Example:
//
// result := statereaderioresult.FromResult[AppState](result.Of(42))
func FromResult[S, A any](ma Result[A]) StateReaderIOResult[S, A] {
return result.Fold(Left[S, A], Right[S, A])(ma)
}
// Combinators
// Local runs a computation with a modified context.
// The function f transforms the context before passing it to the computation.
//
// Example:
//
// // Modify context before running computation
// withTimeout := statereaderioresult.Local[AppState](
// func(ctx context.Context) context.Context {
// ctx, _ = context.WithTimeout(ctx, 60*time.Second)
// return ctx
// }
// )
// result := withTimeout(computation)
func Local[S, A any](f func(context.Context) context.Context) func(StateReaderIOResult[S, A]) StateReaderIOResult[S, A] {
return func(ma StateReaderIOResult[S, A]) StateReaderIOResult[S, A] {
return function.Flow2(ma, RIOR.Local[Pair[S, A]](f))
}
}
// Asks creates a computation that derives a value from the context.
// The function receives the context and returns a StateReaderIOResult.
//
// Example:
//
// getValue := statereaderioresult.Asks[AppState, string](
// func(ctx context.Context) statereaderioresult.StateReaderIOResult[AppState, string] {
// return statereaderioresult.Of[AppState](ctx.Value("key").(string))
// },
// )
func Asks[S, A any](f func(context.Context) StateReaderIOResult[S, A]) StateReaderIOResult[S, A] {
return func(s S) ReaderIOResult[Pair[S, A]] {
return func(ctx context.Context) IOResult[Pair[S, A]] {
return f(ctx)(s)(ctx)
}
}
}
// FromResultK lifts a Result-returning function into a Kleisli arrow for StateReaderIOResult.
//
// Example:
//
// validate := func(x int) result.Result[int] {
// if x > 0 { return result.Of(x) }
// return result.Error[int](errors.New("negative"))
// }
// kleisli := statereaderioresult.FromResultK[AppState](validate)
func FromResultK[S, A, B any](f func(A) Result[B]) Kleisli[S, A, B] {
return function.Flow2(
f,
FromResult[S, B],
)
}
// FromIOK lifts an IO-returning function into a Kleisli arrow for StateReaderIOResult.
func FromIOK[S, A, B any](f func(A) IO[B]) Kleisli[S, A, B] {
return function.Flow2(
f,
FromIO[S, B],
)
}
// FromIOResultK lifts an IOResult-returning function into a Kleisli arrow for StateReaderIOResult.
func FromIOResultK[S, A, B any](f func(A) IOResult[B]) Kleisli[S, A, B] {
return function.Flow2(
f,
FromIOResult[S, B],
)
}
// FromReaderIOResultK lifts a ReaderIOResult-returning function into a Kleisli arrow for StateReaderIOResult.
func FromReaderIOResultK[S, A, B any](f func(A) ReaderIOResult[B]) Kleisli[S, A, B] {
return function.Flow2(
f,
FromReaderIOResult[S, B],
)
}
// MonadChainReaderIOResultK chains a StateReaderIOResult with a ReaderIOResult-returning function.
func MonadChainReaderIOResultK[S, A, B any](ma StateReaderIOResult[S, A], f func(A) ReaderIOResult[B]) StateReaderIOResult[S, B] {
return MonadChain(ma, FromReaderIOResultK[S](f))
}
// ChainReaderIOResultK is the curried version of [MonadChainReaderIOResultK].
func ChainReaderIOResultK[S, A, B any](f func(A) ReaderIOResult[B]) Operator[S, A, B] {
return Chain(FromReaderIOResultK[S](f))
}
// MonadChainIOResultK chains a StateReaderIOResult with an IOResult-returning function.
func MonadChainIOResultK[S, A, B any](ma StateReaderIOResult[S, A], f func(A) IOResult[B]) StateReaderIOResult[S, B] {
return MonadChain(ma, FromIOResultK[S](f))
}
// ChainIOResultK is the curried version of [MonadChainIOResultK].
func ChainIOResultK[S, A, B any](f func(A) IOResult[B]) Operator[S, A, B] {
return Chain(FromIOResultK[S](f))
}
// MonadChainResultK chains a StateReaderIOResult with a Result-returning function.
func MonadChainResultK[S, A, B any](ma StateReaderIOResult[S, A], f func(A) Result[B]) StateReaderIOResult[S, B] {
return MonadChain(ma, FromResultK[S](f))
}
// ChainResultK is the curried version of [MonadChainResultK].
func ChainResultK[S, A, B any](f func(A) Result[B]) Operator[S, A, B] {
return Chain(FromResultK[S](f))
}

View File

@@ -0,0 +1,567 @@
// 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 statereaderioresult
import (
"context"
"errors"
"fmt"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
IOR "github.com/IBM/fp-go/v2/ioresult"
N "github.com/IBM/fp-go/v2/number"
P "github.com/IBM/fp-go/v2/pair"
RES "github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
type testState struct {
counter int
}
func TestOf(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
result := Of[testState](42)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Fold(
func(err error) bool {
t.Fatalf("Expected Success but got Error: %v", err)
return false
},
func(p P.Pair[testState, int]) bool {
assert.Equal(t, 42, P.Tail(p))
assert.Equal(t, 0, P.Head(p).counter) // State unchanged
return true
},
)(res)
}
func TestRight(t *testing.T) {
state := testState{counter: 5}
ctx := context.Background()
result := Right[testState](100)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 100, P.Tail(p))
assert.Equal(t, 5, P.Head(p).counter)
return p
})(res)
}
func TestLeft(t *testing.T) {
state := testState{counter: 10}
ctx := context.Background()
testErr := errors.New("test error")
result := Left[testState, int](testErr)
res := result(state)(ctx)()
assert.True(t, RES.IsLeft(res))
}
func TestMonadMap(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
result := MonadMap(
Of[testState](21),
N.Mul(2),
)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 42, P.Tail(p))
return p
})(res)
}
func TestMap(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
result := F.Pipe1(
Of[testState](21),
Map[testState](N.Mul(2)),
)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 42, P.Tail(p))
return p
})(res)
}
func TestMonadChain(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
result := MonadChain(
Of[testState](5),
func(x int) StateReaderIOResult[testState, string] {
return Of[testState](fmt.Sprintf("value: %d", x))
},
)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] {
assert.Equal(t, "value: 5", P.Tail(p))
return p
})(res)
}
func TestChain(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
result := F.Pipe1(
Of[testState](5),
Chain(func(x int) StateReaderIOResult[testState, string] {
return Of[testState](fmt.Sprintf("value: %d", x))
}),
)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] {
assert.Equal(t, "value: 5", P.Tail(p))
return p
})(res)
}
func TestMonadAp(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
fab := Of[testState](N.Mul(2))
fa := Of[testState](21)
result := MonadAp(fab, fa)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 42, P.Tail(p))
return p
})(res)
}
func TestAp(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
fa := Of[testState](21)
result := F.Pipe1(
Of[testState](N.Mul(2)),
Ap[int](fa),
)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 42, P.Tail(p))
return p
})(res)
}
func TestFromIOResult(t *testing.T) {
state := testState{counter: 3}
ctx := context.Background()
ior := IOR.Of(55)
result := FromIOResult[testState](ior)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 55, P.Tail(p))
assert.Equal(t, 3, P.Head(p).counter)
return p
})(res)
}
func TestFromState(t *testing.T) {
initialState := testState{counter: 10}
ctx := context.Background()
// State computation that increments counter and returns it
stateComp := func(s testState) P.Pair[testState, int] {
newState := testState{counter: s.counter + 1}
return P.MakePair(newState, newState.counter)
}
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.Head(p).counter) // State updated
return p
})(res)
}
func TestFromIO(t *testing.T) {
state := testState{counter: 8}
ctx := context.Background()
ioVal := func() int { return 99 }
result := FromIO[testState](ioVal)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 99, P.Tail(p))
assert.Equal(t, 8, P.Head(p).counter)
return p
})(res)
}
func TestFromResult(t *testing.T) {
state := testState{counter: 12}
ctx := context.Background()
// Test Success case
resultSuccess := FromResult[testState](RES.Of(42))
resSuccess := resultSuccess(state)(ctx)()
assert.True(t, RES.IsRight(resSuccess))
// Test Error case
resultError := FromResult[testState](RES.Left[int](errors.New("error")))
resError := resultError(state)(ctx)()
assert.True(t, RES.IsLeft(resError))
}
func TestLocal(t *testing.T) {
state := testState{counter: 0}
ctx := context.WithValue(context.Background(), "key", "value1")
// Create a computation that uses the context
comp := Asks(func(c context.Context) StateReaderIOResult[testState, string] {
val := c.Value("key").(string)
return Of[testState](val)
})
// Modify context before running computation
result := Local[testState, string](
func(c context.Context) context.Context {
return context.WithValue(c, "key", "value2")
},
)(comp)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] {
assert.Equal(t, "value2", P.Tail(p))
return p
})(res)
}
func TestAsks(t *testing.T) {
state := testState{counter: 0}
ctx := context.WithValue(context.Background(), "multiplier", 7)
result := Asks(func(c context.Context) StateReaderIOResult[testState, int] {
mult := c.Value("multiplier").(int)
return Of[testState](mult * 5)
})
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 35, P.Tail(p))
return p
})(res)
}
func TestFromResultK(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
validate := func(x int) RES.Result[int] {
if x > 0 {
return RES.Of(x * 2)
}
return RES.Left[int](errors.New("negative"))
}
kleisli := FromResultK[testState](validate)
// Test with valid input
resultValid := kleisli(5)
resValid := resultValid(state)(ctx)()
assert.True(t, RES.IsRight(resValid))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 10, P.Tail(p))
return p
})(resValid)
// Test with invalid input
resultInvalid := kleisli(-5)
resInvalid := resultInvalid(state)(ctx)()
assert.True(t, RES.IsLeft(resInvalid))
}
func TestFromIOK(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
ioFunc := func(x int) io.IO[int] {
return func() int { return x * 3 }
}
kleisli := FromIOK[testState](ioFunc)
result := kleisli(7)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 21, P.Tail(p))
return p
})(res)
}
func TestFromIOResultK(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
iorFunc := func(x int) IOR.IOResult[int] {
if x > 0 {
return IOR.Of(x * 4)
}
return IOR.Left[int](errors.New("invalid"))
}
kleisli := FromIOResultK[testState](iorFunc)
result := kleisli(3)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 12, P.Tail(p))
return p
})(res)
}
func TestChainResultK(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
validate := func(x int) RES.Result[string] {
if x > 0 {
return RES.Of(fmt.Sprintf("valid: %d", x))
}
return RES.Left[string](errors.New("invalid"))
}
result := F.Pipe1(
Of[testState](42),
ChainResultK[testState](validate),
)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] {
assert.Equal(t, "valid: 42", P.Tail(p))
return p
})(res)
}
func TestChainIOResultK(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
iorFunc := func(x int) IOR.IOResult[string] {
return IOR.Of(fmt.Sprintf("result: %d", x))
}
result := F.Pipe1(
Of[testState](100),
ChainIOResultK[testState](iorFunc),
)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] {
assert.Equal(t, "result: 100", P.Tail(p))
return p
})(res)
}
func TestDo(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
type Result struct {
value int
}
result := Do[testState](Result{})
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, Result]) P.Pair[testState, Result] {
assert.Equal(t, 0, P.Tail(p).value)
return p
})(res)
}
func TestBindTo(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
type Result struct {
value int
}
result := F.Pipe1(
Of[testState](42),
BindTo[testState](func(v int) Result {
return Result{value: v}
}),
)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, Result]) P.Pair[testState, Result] {
assert.Equal(t, 42, P.Tail(p).value)
return p
})(res)
}
func TestStatefulComputation(t *testing.T) {
initialState := testState{counter: 0}
ctx := context.Background()
// Create a computation that modifies state
incrementAndGet := func(s testState) P.Pair[testState, int] {
newState := testState{counter: s.counter + 1}
return P.MakePair(newState, newState.counter)
}
// Chain multiple stateful operations
result := F.Pipe2(
FromState(incrementAndGet),
Chain(func(v1 int) StateReaderIOResult[testState, int] {
return FromState(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.Head(p).counter) // State updated three times
return p
})(res)
}
func TestErrorPropagation(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
testErr := errors.New("test error")
// Chain operations where the second one fails
result := F.Pipe1(
Of[testState](42),
Chain(func(x int) StateReaderIOResult[testState, int] {
return Left[testState, int](testErr)
}),
)
res := result(state)(ctx)()
assert.True(t, RES.IsLeft(res))
}
func TestPointed(t *testing.T) {
p := Pointed[testState, int]()
assert.NotNil(t, p)
result := p.Of(42)
state := testState{counter: 0}
ctx := context.Background()
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
}
func TestFunctor(t *testing.T) {
f := Functor[testState, int, string]()
assert.NotNil(t, f)
mapper := f.Map(func(x int) string { return fmt.Sprintf("%d", x) })
result := mapper(Of[testState](42))
state := testState{counter: 0}
ctx := context.Background()
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] {
assert.Equal(t, "42", P.Tail(p))
return p
})(res)
}
func TestApplicative(t *testing.T) {
a := Applicative[testState, int, string]()
assert.NotNil(t, a)
fab := Of[testState](func(x int) string { return fmt.Sprintf("%d", x) })
fa := Of[testState](42)
result := a.Ap(fa)(fab)
state := testState{counter: 0}
ctx := context.Background()
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] {
assert.Equal(t, "42", P.Tail(p))
return p
})(res)
}
func TestMonad(t *testing.T) {
m := Monad[testState, int, string]()
assert.NotNil(t, m)
fa := m.Of(42)
result := m.Chain(func(x int) StateReaderIOResult[testState, string] {
return Of[testState](fmt.Sprintf("%d", x))
})(fa)
state := testState{counter: 0}
ctx := context.Background()
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] {
assert.Equal(t, "42", P.Tail(p))
return p
})(res)
}

View File

@@ -0,0 +1,87 @@
// 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 testing
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"
)
// AssertLaws asserts the monad laws for the StateReaderIOResult monad
func AssertLaws[S, A, B, C any](t *testing.T,
eqs EQ.Eq[S],
eqa EQ.Eq[A],
eqb EQ.Eq[B],
eqc EQ.Eq[C],
ab func(A) B,
bc func(B) C,
s S,
ctx context.Context,
) func(a A) bool {
eqra := RIORES.Eq(RES.Eq(P.Eq(eqs, eqa)))(ctx)
eqrb := RIORES.Eq(RES.Eq(P.Eq(eqs, eqb)))(ctx)
eqrc := RIORES.Eq(RES.Eq(P.Eq(eqs, eqc)))(ctx)
fofc := ST.Pointed[S, C]()
fofaa := ST.Pointed[S, func(A) A]()
fofbc := ST.Pointed[S, func(B) C]()
fofabb := ST.Pointed[S, func(func(A) B) B]()
fmap := ST.Functor[S, func(B) C, func(func(A) B) func(A) C]()
fapabb := ST.Applicative[S, func(A) B, B]()
fapabac := ST.Applicative[S, func(A) B, func(A) C]()
maa := ST.Monad[S, A, A]()
mab := ST.Monad[S, A, B]()
mac := ST.Monad[S, A, C]()
mbc := ST.Monad[S, B, C]()
return L.MonadAssertLaws(t,
ST.Eq(eqra)(s),
ST.Eq(eqrb)(s),
ST.Eq(eqrc)(s),
fofc,
fofaa,
fofbc,
fofabb,
fmap,
fapabb,
fapabac,
maa,
mab,
mac,
mbc,
ab,
bc,
)
}

View File

@@ -0,0 +1,50 @@
// 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 testing
import (
"context"
"fmt"
"testing"
A "github.com/IBM/fp-go/v2/array"
EQ "github.com/IBM/fp-go/v2/eq"
"github.com/stretchr/testify/assert"
)
func TestMonadLaws(t *testing.T) {
// some comparison
eqs := A.Eq(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, eqs, eqa, eqb, eqc, ab, bc, A.Empty[string](), context.Background())
assert.True(t, laws(true))
assert.True(t, laws(false))
}

View File

@@ -0,0 +1,84 @@
// 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 statereaderioresult
import (
RIORES "github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/optics/iso/lens"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/state"
)
type (
// Endomorphism represents a function from A to A.
Endomorphism[A any] = endomorphism.Endomorphism[A]
// Lens is an optic that focuses on a field of type A within a structure of type S.
Lens[S, A any] = lens.Lens[S, A]
// State represents a stateful computation that takes an initial state S and returns
// a pair of the new state S and a value A.
State[S, A any] = state.State[S, A]
// Pair represents a tuple of two values.
Pair[L, R any] = pair.Pair[L, R]
// Reader represents a computation that depends on an environment/context of type R
// and produces a value of type A.
Reader[R, A any] = reader.Reader[R, A]
// Result represents a value that can be either an error or a success value.
// This is specialized to use [error] as the error type.
Result[A any] = result.Result[A]
// IO represents a computation that performs side effects and produces a value of type A.
IO[A any] = io.IO[A]
// IOResult represents a computation that performs side effects and can fail with an error
// or succeed with a value A.
IOResult[A any] = ioresult.IOResult[A]
// ReaderIOResult represents a computation that depends on a context.Context,
// performs side effects, and can fail with an error or succeed with a value A.
ReaderIOResult[A any] = RIORES.ReaderIOResult[A]
// StateReaderIOResult represents a stateful computation that:
// - Takes an initial state S
// - Depends on a [context.Context]
// - Performs side effects (IO)
// - Can fail with an [error] or succeed with a value A
// - Returns a pair of the new state S and the result
//
// This is the main type of this package, combining State, Reader, IO, and Result monads.
// It is a specialization of StateReaderIOEither with:
// - Context type fixed to [context.Context]
// - Error type fixed to [error]
StateReaderIOResult[S, A any] = Reader[S, ReaderIOResult[Pair[S, A]]]
// Kleisli represents a Kleisli arrow - a function that takes a value A and returns
// a StateReaderIOResult computation producing B.
// This is used for monadic composition via Chain.
Kleisli[S, A, B any] = Reader[A, StateReaderIOResult[S, B]]
// Operator represents a function that transforms one StateReaderIOResult into another.
// This is commonly used for building composable operations via Map, Chain, etc.
Operator[S, A, B any] = Reader[StateReaderIOResult[S, A], StateReaderIOResult[S, B]]
)

67
v2/coverage.txt Normal file
View 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

View File

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

View File

@@ -39,7 +39,10 @@ package either
// // Use file here
// return either.Right[error]("data")
// })
func WithResource[E, R, A, ANY any](onCreate func() Either[E, R], onRelease Kleisli[E, R, ANY]) Kleisli[E, Kleisli[E, R, A], A] {
func WithResource[A, E, R, ANY any](
onCreate func() Either[E, R],
onRelease Kleisli[E, R, ANY],
) Kleisli[E, Kleisli[E, R, A], A] {
return func(f func(R) Either[E, A]) Either[E, A] {
r := onCreate()
if r.isLeft {

View File

@@ -40,7 +40,7 @@ func TestWithResource(t *testing.T) {
return Of[error](f.Name())
}
tempFile := WithResource[error, *os.File, string](onCreate, onDelete)
tempFile := WithResource[string](onCreate, onDelete)
resE := tempFile(onHandler)

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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.

View File

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

View 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`*

View 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,
)
}

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

View 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

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

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

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

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

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

View 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

View 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

View 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

View 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]())
}

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

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

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

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

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

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

View 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]
)

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

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

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

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

View 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]),
)
}

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

View File

@@ -0,0 +1,44 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package 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)
}

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

View 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]
)

View 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])
}

View 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

File diff suppressed because it is too large Load Diff

View 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
}),
)
}

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

View 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]
)

View 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]),
)
}

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

View 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]
}
)

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

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

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

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

View 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],
},
}
}

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

View 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,
)
}

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

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

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

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

View 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],
)
}

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

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

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

View 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,
)
}

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

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

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

View 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]
)

View File

@@ -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++ {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

Some files were not shown because too many files have changed in this diff Show More