mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-09 23:11:40 +02:00
Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1704b6d26 | ||
|
|
ffdfd218f8 | ||
|
|
34826d8c52 | ||
|
|
24c0519cc7 | ||
|
|
ff48d8953e | ||
|
|
d739c9b277 | ||
|
|
f0054431a5 | ||
|
|
1a89ec3df7 | ||
|
|
f652a94c3a | ||
|
|
774db88ca5 | ||
|
|
62a3365b20 | ||
|
|
d9a16a6771 | ||
|
|
8949cc7dca | ||
|
|
fa6b6caf22 | ||
|
|
a1e8d397c3 | ||
|
|
dbe7102e43 | ||
|
|
09aeb996e2 | ||
|
|
7cd575d95a | ||
|
|
dcfb023891 | ||
|
|
51cf241a26 | ||
|
|
9004c93976 | ||
|
|
d8ab6b0ce5 | ||
|
|
4e9998b645 | ||
|
|
2ea9e292e1 | ||
|
|
12a20e30d1 | ||
|
|
4909ad5473 | ||
|
|
d116317cde | ||
|
|
1428241f2c | ||
|
|
ef9216bad7 | ||
|
|
fe77c770b6 | ||
|
|
1c42b2ac1d | ||
|
|
cbd93fdecc | ||
|
|
6d94697128 | ||
|
|
77dde302ef | ||
|
|
909d626019 | ||
|
|
b01a8f2aff | ||
|
|
8a2e9539b1 | ||
|
|
03d9720a29 | ||
|
|
57794ccb34 | ||
|
|
404eb875d3 | ||
|
|
ed108812d6 | ||
|
|
ab868315d4 | ||
|
|
02d0be9dad | ||
|
|
2c1d8196b4 | ||
|
|
17eb8ae66f | ||
|
|
b70e481e7d | ||
|
|
3c3bb7c166 | ||
|
|
d3007cbbfa | ||
|
|
5aa0e1ea2e | ||
|
|
d586428cb0 | ||
|
|
d2dbce6e8b | ||
|
|
6f7ec0768d | ||
|
|
ca813b673c | ||
|
|
af271e7d10 | ||
|
|
567315a31c | ||
|
|
311ed55f06 | ||
|
|
23333ce52c | ||
|
|
eb7fc9f77b | ||
|
|
fd0550e71b | ||
|
|
13063bbd88 | ||
|
|
4f8a557072 | ||
|
|
a4e790ac3d | ||
|
|
1af6501cd8 | ||
|
|
600521b220 | ||
|
|
62fcd186a3 | ||
|
|
2db7e83651 | ||
|
|
8d92df83ad | ||
|
|
0c742b81e6 | ||
|
|
a1d6c94b15 | ||
|
|
e4e28a6556 | ||
|
|
7e7cc06f11 | ||
|
|
54d5dbd04a | ||
|
|
51adce0c95 | ||
|
|
aa5e908810 | ||
|
|
b3bd5e9ad3 | ||
|
|
178df09ff7 | ||
|
|
92eb9715bd | ||
|
|
41ebb04ae0 | ||
|
|
b2705e3adf | ||
|
|
b232183e47 | ||
|
|
0f9f89f16d |
37
.github/workflows/build.yml
vendored
37
.github/workflows/build.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
fail-fast: false # Continue with other versions if one fails
|
||||
steps:
|
||||
# full checkout for semantic-release
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go ${{ matrix.go-version }}
|
||||
@@ -41,6 +41,14 @@ jobs:
|
||||
go mod tidy
|
||||
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
- name: Upload coverage to Coveralls
|
||||
uses: coverallsapp/github-action@v2
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ./coverage.txt
|
||||
flag-name: v1-go-${{ matrix.go-version }}
|
||||
parallel: true
|
||||
|
||||
# - name: Upload coverage to Codecov
|
||||
# uses: codecov/codecov-action@v5
|
||||
# with:
|
||||
@@ -58,7 +66,7 @@ jobs:
|
||||
matrix:
|
||||
go-version: ['1.24.x', '1.25.x']
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go ${{ matrix.go-version }}
|
||||
@@ -72,6 +80,14 @@ jobs:
|
||||
go mod tidy
|
||||
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
- name: Upload coverage to Coveralls
|
||||
uses: coverallsapp/github-action@v2
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ./v2/coverage.txt
|
||||
flag-name: v2-go-${{ matrix.go-version }}
|
||||
parallel: true
|
||||
|
||||
# - name: Upload coverage to Codecov
|
||||
# uses: codecov/codecov-action@v5
|
||||
# with:
|
||||
@@ -82,9 +98,22 @@ jobs:
|
||||
# env:
|
||||
# CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
coveralls-finish:
|
||||
name: Finish Coveralls
|
||||
needs:
|
||||
- build-v1
|
||||
- build-v2
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Coveralls Finished
|
||||
uses: coverallsapp/github-action@v2
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
parallel-finished: true
|
||||
|
||||
release:
|
||||
name: Release
|
||||
needs:
|
||||
needs:
|
||||
- build-v1
|
||||
- build-v2
|
||||
if: github.repository == 'IBM/fp-go' && github.event_name != 'pull_request'
|
||||
@@ -97,7 +126,7 @@ jobs:
|
||||
steps:
|
||||
# full checkout for semantic-release
|
||||
- name: Full checkout
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,4 +2,5 @@ fp-go.exe
|
||||
fp-go
|
||||
main.exe
|
||||
build/
|
||||
.idea
|
||||
.idea
|
||||
*.exe
|
||||
347
README.md
347
README.md
@@ -1,207 +1,312 @@
|
||||
# Functional programming library for golang
|
||||
# fp-go: Functional Programming Library for Go
|
||||
|
||||
**🚧 Work in progress! 🚧** Despite major version 1 because of <https://github.com/semantic-release/semantic-release/issues/1507>. Trying to not make breaking changes, but devil is in the details.
|
||||
[](https://pkg.go.dev/github.com/IBM/fp-go)
|
||||
[](https://coveralls.io/github/IBM/fp-go?branch=main)
|
||||
|
||||
**🚧 Work in progress! 🚧** Despite major version 1 (due to [semantic-release limitations](https://github.com/semantic-release/semantic-release/issues/1507)), we're working to minimize breaking changes.
|
||||
|
||||

|
||||
|
||||
This library is strongly influenced by the awesome [fp-ts](https://github.com/gcanti/fp-ts).
|
||||
A comprehensive functional programming library for Go, strongly influenced by the excellent [fp-ts](https://github.com/gcanti/fp-ts) library for TypeScript.
|
||||
|
||||
## Getting started
|
||||
## 📚 Table of Contents
|
||||
|
||||
- [Getting Started](#-getting-started)
|
||||
- [Design Goals](#-design-goals)
|
||||
- [Core Concepts](#-core-concepts)
|
||||
- [Comparison to Idiomatic Go](#comparison-to-idiomatic-go)
|
||||
- [Implementation Notes](#implementation-notes)
|
||||
- [Common Operations](#common-operations)
|
||||
- [Resources](#-resources)
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
go get github.com/IBM/fp-go
|
||||
```
|
||||
|
||||
Refer to the [samples](./samples/).
|
||||
### Quick Example
|
||||
|
||||
Find API documentation [here](https://pkg.go.dev/github.com/IBM/fp-go)
|
||||
```go
|
||||
import (
|
||||
"errors"
|
||||
"github.com/IBM/fp-go/either"
|
||||
"github.com/IBM/fp-go/function"
|
||||
)
|
||||
|
||||
## Design Goal
|
||||
// Pure function that can fail
|
||||
func divide(a, b int) either.Either[error, int] {
|
||||
if b == 0 {
|
||||
return either.Left[int](errors.New("division by zero"))
|
||||
}
|
||||
return either.Right[error](a / b)
|
||||
}
|
||||
|
||||
This library aims to provide a set of data types and functions that make it easy and fun to write maintainable and testable code in golang. It encourages the following patterns:
|
||||
// Compose operations safely
|
||||
result := function.Pipe2(
|
||||
divide(10, 2),
|
||||
either.Map(func(x int) int { return x * 2 }),
|
||||
either.GetOrElse(func() int { return 0 }),
|
||||
)
|
||||
// result = 10
|
||||
```
|
||||
|
||||
- write many small, testable and pure functions, i.e. functions that produce output only depending on their input and that do not execute side effects
|
||||
- offer helpers to isolate side effects into lazily executed functions (IO)
|
||||
- expose a consistent set of composition to create new functions from existing ones
|
||||
- for each data type there exists a small set of composition functions
|
||||
- these functions are called the same across all data types, so you only have to learn a small number of function names
|
||||
- the semantic of functions of the same name is consistent across all data types
|
||||
### Resources
|
||||
|
||||
### How does this play with the [🧘🏽 Zen Of Go](https://the-zen-of-go.netlify.app/)?
|
||||
- 📖 [API Documentation](https://pkg.go.dev/github.com/IBM/fp-go)
|
||||
- 💡 [Code Samples](./samples/)
|
||||
- 🆕 [V2 Documentation](./v2/README.md) (requires Go 1.24+)
|
||||
|
||||
#### 🧘🏽 Each package fulfills a single purpose
|
||||
## 🎯 Design Goals
|
||||
|
||||
✔️ Each of the top level packages (e.g. Option, Either, ReaderIOEither, ...) fulfills the purpose of defining the respective data type and implementing the set of common operations for this data type.
|
||||
This library aims to provide a set of data types and functions that make it easy and fun to write maintainable and testable code in Go. It encourages the following patterns:
|
||||
|
||||
#### 🧘🏽 Handle errors explicitly
|
||||
### Core Principles
|
||||
|
||||
✔️ The library makes a clear distinction between that operations that cannot fail by design and operations that can fail. Failure is represented via the `Either` type and errors are handled explicitly by using `Either`'s monadic set of operations.
|
||||
- **Pure Functions**: Write many small, testable, and pure functions that produce output only depending on their input and execute no side effects
|
||||
- **Side Effect Isolation**: Isolate side effects into lazily executed functions using the `IO` monad
|
||||
- **Consistent Composition**: Expose a consistent set of composition functions across all data types
|
||||
- Each data type has a small set of composition functions
|
||||
- Functions are named consistently across all data types
|
||||
- Semantics of same-named functions are consistent across data types
|
||||
|
||||
#### 🧘🏽 Return early rather than nesting deeply
|
||||
### 🧘🏽 Alignment with the Zen of Go
|
||||
|
||||
✔️ We recommend to implement simple, small functions that implement one feature and that would typically not invoke other functions. Interaction with other functions is done by function composition and the composition makes sure to run one function after the other. In the error case the `Either` monad makes sure to skip the error path.
|
||||
This library respects and aligns with [The Zen of Go](https://the-zen-of-go.netlify.app/):
|
||||
|
||||
#### 🧘🏽 Leave concurrency to the caller
|
||||
| Principle | Alignment | Explanation |
|
||||
|-----------|-----------|-------------|
|
||||
| 🧘🏽 Each package fulfills a single purpose | ✔️ | Each top-level package (Option, Either, ReaderIOEither, etc.) defines one data type and its operations |
|
||||
| 🧘🏽 Handle errors explicitly | ✔️ | Clear distinction between operations that can/cannot fail; failures represented via `Either` type |
|
||||
| 🧘🏽 Return early rather than nesting deeply | ✔️ | Small, focused functions composed together; `Either` monad handles error paths automatically |
|
||||
| 🧘🏽 Leave concurrency to the caller | ✔️ | Pure functions are synchronous; I/O operations are asynchronous by default |
|
||||
| 🧘🏽 Before you launch a goroutine, know when it will stop | 🤷🏽 | Library doesn't start goroutines; Task monad supports cancellation via context |
|
||||
| 🧘🏽 Avoid package level state | ✔️ | No package-level state anywhere |
|
||||
| 🧘🏽 Simplicity matters | ✔️ | Small, consistent interface across data types; focus on business logic |
|
||||
| 🧘🏽 Write tests to lock in behaviour | 🟡 | Programming pattern encourages testing; library has growing test coverage |
|
||||
| 🧘🏽 If you think it's slow, first prove it with a benchmark | ✔️ | Performance claims should be backed by benchmarks |
|
||||
| 🧘🏽 Moderation is a virtue | ✔️ | No custom goroutines or expensive synchronization; atomic counters for coordination |
|
||||
| 🧘🏽 Maintainability counts | ✔️ | Small, concise operations; pure functions with clear type signatures |
|
||||
|
||||
✔️ All pure are synchronous by default. The I/O operations are asynchronous per default.
|
||||
## 💡 Core Concepts
|
||||
|
||||
#### 🧘🏽 Before you launch a goroutine, know when it will stop
|
||||
### Data Types
|
||||
|
||||
🤷🏽 This is left to the user of the library since the library itself will not start goroutines on its own. The Task monad offers support for cancellation via the golang context, though.
|
||||
The library provides several key functional data types:
|
||||
|
||||
#### 🧘🏽 Avoid package level state
|
||||
- **`Option[A]`**: Represents an optional value (Some or None)
|
||||
- **`Either[E, A]`**: Represents a value that can be one of two types (Left for errors, Right for success)
|
||||
- **`IO[A]`**: Represents a lazy computation that produces a value
|
||||
- **`IOEither[E, A]`**: Represents a lazy computation that can fail
|
||||
- **`Reader[R, A]`**: Represents a computation that depends on an environment
|
||||
- **`ReaderIOEither[R, E, A]`**: Combines Reader, IO, and Either for effectful computations with dependencies
|
||||
- **`Task[A]`**: Represents an asynchronous computation
|
||||
- **`State[S, A]`**: Represents a stateful computation
|
||||
|
||||
✔️ No package level state anywhere, this would be a significant anti-pattern
|
||||
### Monadic Operations
|
||||
|
||||
#### 🧘🏽 Simplicity matters
|
||||
All data types support common monadic operations:
|
||||
|
||||
✔️ The library is simple in the sense that it offers a small, consistent interface to a variety of data types. Users can concentrate on implementing business logic rather than dealing with low level data structures.
|
||||
|
||||
#### 🧘🏽 Write tests to lock in the behaviour of your package’s API
|
||||
|
||||
🟡 The programming pattern suggested by this library encourages writing test cases. The library itself also has a growing number of tests, but not enough, yet. TBD
|
||||
|
||||
#### 🧘🏽 If you think it’s slow, first prove it with a benchmark
|
||||
|
||||
✔️ Absolutely. If you think the function composition offered by this library is too slow, please provide a benchmark.
|
||||
|
||||
#### 🧘🏽 Moderation is a virtue
|
||||
|
||||
✔️ The library does not implement its own goroutines and also does not require any expensive synchronization primitives. Coordination of IO operations is implemented via atomic counters without additional primitives.
|
||||
|
||||
#### 🧘🏽 Maintainability counts
|
||||
|
||||
✔️ Code that consumes this library is easy to maintain because of the small and concise set of operations exposed. Also the suggested programming paradigm to decompose an application into small functions increases maintainability, because these functions are easy to understand and if they are pure, it's often sufficient to look at the type signature to understand the purpose.
|
||||
|
||||
The library itself also comprises many small functions, but it's admittedly harder to maintain than code that uses it. However this asymmetry is intended because it offloads complexity from users into a central component.
|
||||
- **`Map`**: Transform the value inside a context
|
||||
- **`Chain`** (FlatMap): Transform and flatten nested contexts
|
||||
- **`Ap`**: Apply a function in a context to a value in a context
|
||||
- **`Of`**: Wrap a value in a context
|
||||
- **`Fold`**: Extract a value from a context
|
||||
|
||||
## Comparison to Idiomatic Go
|
||||
|
||||
In this section we discuss how the functional APIs differ from idiomatic go function signatures and how to convert back and forth.
|
||||
This section explains how functional APIs differ from idiomatic Go and how to convert between them.
|
||||
|
||||
### Pure functions
|
||||
### Pure Functions
|
||||
|
||||
Pure functions are functions that take input parameters and that compute an output without changing any global state and without mutating the input parameters. They will always return the same output for the same input.
|
||||
Pure functions take input parameters and compute output without changing global state or mutating inputs. They always return the same output for the same input.
|
||||
|
||||
#### Without Errors
|
||||
|
||||
If your pure function does not return an error, the idiomatic signature is just fine and no changes are required.
|
||||
If your pure function doesn't return an error, the idiomatic signature works as-is:
|
||||
|
||||
```go
|
||||
func add(a, b int) int {
|
||||
return a + b
|
||||
}
|
||||
```
|
||||
|
||||
#### With Errors
|
||||
|
||||
If your pure function can return an error, then it will have a `(T, error)` return value in idiomatic go. In functional style the return value is [Either[error, T]](https://pkg.go.dev/github.com/IBM/fp-go/either) because function composition is easier with such a return type. Use the `EitherizeXXX` methods in ["github.com/IBM/fp-go/either"](https://pkg.go.dev/github.com/IBM/fp-go/either) to convert from idiomatic to functional style and `UneitherizeXXX` to convert from functional to idiomatic style.
|
||||
**Idiomatic Go:**
|
||||
```go
|
||||
func divide(a, b int) (int, error) {
|
||||
if b == 0 {
|
||||
return 0, errors.New("division by zero")
|
||||
}
|
||||
return a / b, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Effectful functions
|
||||
**Functional Style:**
|
||||
```go
|
||||
func divide(a, b int) either.Either[error, int] {
|
||||
if b == 0 {
|
||||
return either.Left[int](errors.New("division by zero"))
|
||||
}
|
||||
return either.Right[error](a / b)
|
||||
}
|
||||
```
|
||||
|
||||
An effectful function (or function with a side effect) is one that changes data outside the scope of the function or that does not always produce the same output for the same input (because it depends on some external, mutable state). There is no special way in idiomatic go to identify such a function other than documentation. In functional style we represent them as functions that do not take an input but that produce an output. The base type for these functions is [IO[T]](https://pkg.go.dev/github.com/IBM/fp-go/io) because in many cases such functions represent `I/O` operations.
|
||||
**Conversion:**
|
||||
- Use `either.EitherizeXXX` to convert from idiomatic to functional style
|
||||
- Use `either.UneitherizeXXX` to convert from functional to idiomatic style
|
||||
|
||||
### Effectful Functions
|
||||
|
||||
An effectful function changes data outside its scope or doesn't always produce the same output for the same input.
|
||||
|
||||
#### Without Errors
|
||||
|
||||
If your effectful function does not return an error, the functional signature is [IO[T]](https://pkg.go.dev/github.com/IBM/fp-go/io)
|
||||
**Functional signature:** `IO[T]`
|
||||
|
||||
```go
|
||||
func getCurrentTime() io.IO[time.Time] {
|
||||
return func() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### With Errors
|
||||
|
||||
If your effectful function can return an error, the functional signature is [IOEither[error, T]](https://pkg.go.dev/github.com/IBM/fp-go/ioeither). Use `EitherizeXXX` from ["github.com/IBM/fp-go/ioeither"](https://pkg.go.dev/github.com/IBM/fp-go/ioeither) to convert an idiomatic go function to functional style.
|
||||
**Functional signature:** `IOEither[error, T]`
|
||||
|
||||
```go
|
||||
func readFile(path string) ioeither.IOEither[error, []byte] {
|
||||
return func() either.Either[error, []byte] {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return either.Left[[]byte](err)
|
||||
}
|
||||
return either.Right[error](data)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Conversion:**
|
||||
- Use `ioeither.EitherizeXXX` to convert idiomatic Go functions to functional style
|
||||
|
||||
### Go Context
|
||||
|
||||
Functions that take a [context](https://pkg.go.dev/context) are per definition effectful because they depend on the context parameter that is designed to be mutable (it can e.g. be used to cancel a running operation). Furthermore in idiomatic go the parameter is typically passed as the first parameter to a function.
|
||||
Functions that take a `context.Context` are effectful because they depend on mutable context.
|
||||
|
||||
In functional style we isolate the [context](https://pkg.go.dev/context) and represent the nature of the effectful function as an [IOEither[error, T]](https://pkg.go.dev/github.com/IBM/fp-go/ioeither). The resulting type is [ReaderIOEither[T]](https://pkg.go.dev/github.com/IBM/fp-go/context/readerioeither), a function taking a [context](https://pkg.go.dev/context) that returns a function without parameters returning an [Either[error, T]](https://pkg.go.dev/github.com/IBM/fp-go/either). Use the `EitherizeXXX` methods from ["github.com/IBM/fp-go/context/readerioeither"](https://pkg.go.dev/github.com/IBM/fp-go/context/readerioeither) to convert an idiomatic go function with a [context](https://pkg.go.dev/context) to functional style.
|
||||
**Idiomatic Go:**
|
||||
```go
|
||||
func fetchData(ctx context.Context, url string) ([]byte, error) {
|
||||
// implementation
|
||||
}
|
||||
```
|
||||
|
||||
**Functional Style:**
|
||||
```go
|
||||
func fetchData(url string) readerioeither.ReaderIOEither[context.Context, error, []byte] {
|
||||
return func(ctx context.Context) ioeither.IOEither[error, []byte] {
|
||||
return func() either.Either[error, []byte] {
|
||||
// implementation
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Conversion:**
|
||||
- Use `readerioeither.EitherizeXXX` to convert idiomatic Go functions with context to functional style
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Generics
|
||||
|
||||
All monadic operations are implemented via generics, i.e. they offer a type safe way to compose operations. This allows for convenient IDE support and also gives confidence about the correctness of the composition at compile time.
|
||||
All monadic operations use Go generics for type safety:
|
||||
|
||||
Downside is that this will result in different versions of each operation per type, these versions are generated by the golang compiler at build time (unlike type erasure in languages such as Java of TypeScript). This might lead to large binaries for codebases with many different types. If this is a concern, you can always implement type erasure on top, i.e. use the monadic operations with the `any` type as if generics were not supported. You loose type safety, but this might result in smaller binaries.
|
||||
- ✅ **Pros**: Type-safe composition, IDE support, compile-time correctness
|
||||
- ⚠️ **Cons**: May result in larger binaries (different versions per type)
|
||||
- 💡 **Tip**: For binary size concerns, use type erasure with `any` type
|
||||
|
||||
### Ordering of Generic Type Parameters
|
||||
|
||||
In go we need to specify all type parameters of a function on the global function definition, even if the function returns a higher order function and some of the type parameters are only applicable to the higher order function. So the following is not possible:
|
||||
Go requires all type parameters on the global function definition. Parameters that cannot be auto-detected come first:
|
||||
|
||||
```go
|
||||
func Map[A, B any](f func(A) B) [R, E any]func(fa ReaderIOEither[R, E, A]) ReaderIOEither[R, E, B]
|
||||
// Map: B cannot be auto-detected, so it comes first
|
||||
func Map[R, E, A, B any](f func(A) B) func(ReaderIOEither[R, E, A]) ReaderIOEither[R, E, B]
|
||||
|
||||
// Ap: B cannot be auto-detected from the argument
|
||||
func Ap[B, R, E, A any](fa ReaderIOEither[R, E, A]) func(ReaderIOEither[R, E, func(A) B]) ReaderIOEither[R, E, B]
|
||||
```
|
||||
|
||||
Note that the parameters `R` and `E` are not needed by the first level of `Map` but only by the resulting higher order function. Instead we need to specify the following:
|
||||
This ordering maximizes type inference where possible.
|
||||
|
||||
```go
|
||||
func Map[R, E, A, B any](f func(A) B) func(fa ReaderIOEither[R, E, A]) ReaderIOEither[R, E, B]
|
||||
```
|
||||
### Use of the ~ Operator
|
||||
|
||||
which overspecifies `Map` on the global scope. As a result the go compiler will not be able to auto-detect these parameters, it can only auto detect `A` and `B` since they appear in the argument of `Map`. We need to explicitly pass values for these type parameters when `Map` is being used.
|
||||
|
||||
Because of this limitation the order of parameters on a function matters. We want to make sure that we define those parameters that cannot be auto-detected, first, and the parameters that can be auto-detected, last. This can lead to inconsistencies in parameter ordering, but we believe that the gain in convenience is worth it. The parameter order of `Ap` is e.g. different from that of `Map`:
|
||||
|
||||
```go
|
||||
func Ap[B, R, E, A any](fa ReaderIOEither[R, E, A]) func(fab ReaderIOEither[R, E, func(A) B]) ReaderIOEither[R, E, B]
|
||||
```
|
||||
|
||||
because `R`, `E` and `A` can be determined from the argument to `Ap` but `B` cannot.
|
||||
|
||||
### Use of the [~ Operator](https://go.googlesource.com/proposal/+/master/design/47781-parameterized-go-ast.md)
|
||||
|
||||
The FP library attempts to be easy to consume and one aspect of this is the definition of higher level type definitions instead of having to use their low level equivalent. It is e.g. more convenient and readable to use
|
||||
|
||||
```go
|
||||
ReaderIOEither[R, E, A]
|
||||
```
|
||||
|
||||
than
|
||||
|
||||
```go
|
||||
func(R) func() Either.Either[E, A]
|
||||
```
|
||||
|
||||
although both are logically equivalent. At the time of this writing the go type system does not support generic type aliases, only generic type definition, i.e. it is not possible to write:
|
||||
|
||||
```go
|
||||
type ReaderIOEither[R, E, A any] = RD.Reader[R, IOE.IOEither[E, A]]
|
||||
```
|
||||
|
||||
only
|
||||
Go doesn't support generic type aliases (until Go 1.24), only type definitions. The `~` operator allows generic implementations to work with compatible types:
|
||||
|
||||
```go
|
||||
type ReaderIOEither[R, E, A any] RD.Reader[R, IOE.IOEither[E, A]]
|
||||
```
|
||||
|
||||
This makes a big difference, because in the second case the type `ReaderIOEither[R, E, A any]` is considered a completely new type, not compatible to its right hand side, so it's not just a shortcut but a fully new type.
|
||||
**Generic Subpackages:**
|
||||
- Each higher-level type has a `generic` subpackage with fully generic implementations
|
||||
- These are for library extensions, not end-users
|
||||
- Main packages specialize generic implementations for convenience
|
||||
|
||||
From the implementation perspective however there is no reason to restrict the implementation to the new type, it can be generic for all compatible types. The way to express this in go is the [~](https://go.googlesource.com/proposal/+/master/design/47781-parameterized-go-ast.md) operator. This comes with some quite complicated type declarations in some cases, which undermines the goal of the library to be easy to use.
|
||||
### Higher Kinded Types (HKT)
|
||||
|
||||
For that reason there exist sub-packages called `Generic` for all higher level types. These packages contain the fully generic implementation of the operations, preferring abstraction over usability. These packages are not meant to be used by end-users but are meant to be used by library extensions. The implementation for the convenient higher level types specializes the generic implementation for the particular higher level type, i.e. this layer does not contain any business logic but only *type magic*.
|
||||
Go doesn't support HKT natively. This library addresses this by:
|
||||
|
||||
### Higher Kinded Types
|
||||
- Introducing HKTs as individual types (e.g., `HKTA` for `HKT[A]`)
|
||||
- Implementing generic algorithms in the `internal` package
|
||||
- Keeping complexity hidden from end-users
|
||||
|
||||
Go does not support higher kinded types (HKT). Such types occur if a generic type itself is parametrized by another generic type. Example:
|
||||
## Common Operations
|
||||
|
||||
The `Map` operation for `ReaderIOEither` is defined as:
|
||||
### Map/Chain/Ap/Flap
|
||||
|
||||
| Operator | Parameter | Monad | Result | Use Case |
|
||||
| -------- | ---------------- | --------------- | -------- | -------- |
|
||||
| Map | `func(A) B` | `HKT[A]` | `HKT[B]` | Transform value in context |
|
||||
| Chain | `func(A) HKT[B]` | `HKT[A]` | `HKT[B]` | Transform and flatten |
|
||||
| Ap | `HKT[A]` | `HKT[func(A)B]` | `HKT[B]` | Apply function in context |
|
||||
| Flap | `A` | `HKT[func(A)B]` | `HKT[B]` | Apply value to function in context |
|
||||
|
||||
### Example: Chaining Operations
|
||||
|
||||
```go
|
||||
func Map[R, E, A, B any](f func(A) B) func(fa ReaderIOEither[R, E, A]) ReaderIOEither[R, E, B]
|
||||
import (
|
||||
"github.com/IBM/fp-go/either"
|
||||
"github.com/IBM/fp-go/function"
|
||||
)
|
||||
|
||||
result := function.Pipe3(
|
||||
either.Right[error](10),
|
||||
either.Map(func(x int) int { return x * 2 }),
|
||||
either.Chain(func(x int) either.Either[error, int] {
|
||||
if x > 15 {
|
||||
return either.Right[error](x)
|
||||
}
|
||||
return either.Left[int](errors.New("too small"))
|
||||
}),
|
||||
either.GetOrElse(func() int { return 0 }),
|
||||
)
|
||||
```
|
||||
|
||||
and in fact the equivalent operations for all other monads follow the same pattern, we could try to introduce a new type for `ReaderIOEither` (without a parameter) as a HKT, e.g. like so (made-up syntax, does not work in go):
|
||||
## 📚 Resources
|
||||
|
||||
```go
|
||||
func Map[HKT, R, E, A, B any](f func(A) B) func(HKT[R, E, A]) HKT[R, E, B]
|
||||
```
|
||||
- [API Documentation](https://pkg.go.dev/github.com/IBM/fp-go)
|
||||
- [Code Samples](./samples/)
|
||||
- [V2 Documentation](./v2/README.md) - New features in Go 1.24+
|
||||
- [fp-ts](https://github.com/gcanti/fp-ts) - Original TypeScript inspiration
|
||||
|
||||
this would be the completely generic method signature for all possible monads. In particular in many cases it is possible to compose functions independent of the concrete knowledge of the actual `HKT`. From the perspective of a library this is the ideal situation because then a particular algorithm only has to be implemented and tested once.
|
||||
## 🤝 Contributing
|
||||
|
||||
This FP library addresses this by introducing the HKTs as individual types, e.g. `HKT[A]` would be represented as a new generic type `HKTA`. This loses the correlation to the type `A` but allows to implement generic algorithms, at the price of readability.
|
||||
Contributions are welcome! Please feel free to submit issues or pull requests.
|
||||
|
||||
For that reason these implementations are kept in the `internal` package. These are meant to be used by the library itself or by extensions, not by end users.
|
||||
## 📄 License
|
||||
|
||||
## Map/Ap/Flap
|
||||
|
||||
The following table lists the relationship between some selected operators
|
||||
|
||||
| Operator | Parameter | Monad | Result |
|
||||
| -------- | ---------------- | --------------- | -------- |
|
||||
| Map | `func(A) B` | `HKT[A]` | `HKT[B]` |
|
||||
| Chain | `func(A) HKT[B]` | `HKT[A]` | `HKT[B]` |
|
||||
| Ap | `HKT[A]` | `HKT[func(A)B]` | `HKT[B]` |
|
||||
| Flap | `A` | `HKT[func(A)B]` | `HKT[B]` |
|
||||
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
|
||||
|
||||
@@ -21,8 +21,8 @@ import (
|
||||
|
||||
type (
|
||||
either struct {
|
||||
isLeft bool
|
||||
value any
|
||||
isLeft bool
|
||||
}
|
||||
|
||||
// Either defines a data structure that logically holds either an E or an A. The flag discriminates the cases
|
||||
@@ -73,12 +73,12 @@ func IsRight[E, A any](val Either[E, A]) bool {
|
||||
|
||||
// Left creates a new instance of an [Either] representing the left value.
|
||||
func Left[A, E any](value E) Either[E, A] {
|
||||
return Either[E, A]{true, value}
|
||||
return Either[E, A]{value, true}
|
||||
}
|
||||
|
||||
// Right creates a new instance of an [Either] representing the right value.
|
||||
func Right[E, A any](value A) Either[E, A] {
|
||||
return Either[E, A]{false, value}
|
||||
return Either[E, A]{value, false}
|
||||
}
|
||||
|
||||
// MonadFold extracts the values from an [Either] by invoking the [onLeft] callback or the [onRight] callback depending on the case
|
||||
@@ -94,8 +94,7 @@ func Unwrap[E, A any](ma Either[E, A]) (A, E) {
|
||||
if ma.isLeft {
|
||||
var a A
|
||||
return a, ma.value.(E)
|
||||
} else {
|
||||
var e E
|
||||
return ma.value.(A), e
|
||||
}
|
||||
var e E
|
||||
return ma.value.(A), e
|
||||
}
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base",
|
||||
"config:recommended",
|
||||
":dependencyDashboard"
|
||||
],
|
||||
"rangeStrategy": "bump",
|
||||
"packageRules": [
|
||||
{
|
||||
"matchDatasources": [
|
||||
"golang-version"
|
||||
],
|
||||
"matchPackageNames": [
|
||||
"go"
|
||||
],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"matchManagers": [
|
||||
"gomod"
|
||||
@@ -25,15 +34,6 @@
|
||||
],
|
||||
"automerge": true,
|
||||
"groupName": "go dependencies"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"conventional-changelog-conventionalcommits"
|
||||
],
|
||||
"matchUpdateTypes": [
|
||||
"major"
|
||||
],
|
||||
"enabled": false
|
||||
}
|
||||
]
|
||||
}
|
||||
17
v2/.claude/settings.local.json
Normal file
17
v2/.claude/settings.local.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"permissions": {
|
||||
"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 test:*)",
|
||||
"Bash(go doc:*)",
|
||||
"Bash(go tool cover:*)",
|
||||
"Bash(sort:*)",
|
||||
"Bash(tee:*)",
|
||||
"Bash(find:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
482
v2/BENCHMARK_COMPARISON.md
Normal file
482
v2/BENCHMARK_COMPARISON.md
Normal file
@@ -0,0 +1,482 @@
|
||||
# Benchmark Comparison: Idiomatic vs Standard Either/Result
|
||||
|
||||
**Date:** 2025-11-18
|
||||
**System:** AMD Ryzen 7 PRO 7840U w/ Radeon 780M Graphics (16 cores)
|
||||
**Go Version:** go1.23+
|
||||
|
||||
This document provides a detailed performance comparison between the optimized `either` package and the `idiomatic/result` package after recent optimizations to the either package.
|
||||
|
||||
## Executive Summary
|
||||
|
||||
After optimizations to the `either` package, the performance characteristics have changed significantly:
|
||||
|
||||
### Key Findings
|
||||
|
||||
1. **Constructors & Predicates**: Both packages now perform comparably (~1-2 ns/op) with **zero heap allocations**
|
||||
2. **Zero-allocation insight**: The `Either` struct (24 bytes) does NOT escape to heap - Go returns it by value on the stack
|
||||
3. **Core Operations**: Idiomatic package has a **consistent advantage** of 1.2x - 2.3x for most operations
|
||||
4. **Complex Operations**: Idiomatic package shows **massive advantages**:
|
||||
- ChainFirst (Right): **32.4x faster** (87.6 ns → 2.7 ns, 72 B → 0 B)
|
||||
- Pipeline operations: **2-3x faster** with lower allocations
|
||||
5. **All simple operations**: Both maintain **zero heap allocations** (0 B/op, 0 allocs/op)
|
||||
|
||||
### Winner by Category
|
||||
|
||||
| Category | Winner | Reason |
|
||||
|----------|--------|--------|
|
||||
| Constructors | **TIE** | Both ~1.3-1.8 ns/op |
|
||||
| Predicates | **TIE** | Both ~1.2-1.5 ns/op |
|
||||
| Simple Transformations | **Idiomatic** | 1.2-2x faster |
|
||||
| Monadic Operations | **Idiomatic** | 1.2-2.3x faster |
|
||||
| Complex Chains | **Idiomatic** | 32x faster, zero allocs |
|
||||
| Pipelines | **Idiomatic** | 2-2.4x faster, fewer allocs |
|
||||
| Extraction | **Idiomatic** | 6x faster (GetOrElse) |
|
||||
|
||||
## Detailed Benchmark Results
|
||||
|
||||
### Constructor Operations
|
||||
|
||||
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|
||||
|-----------|----------------|-------------------|---------|---------------|-------------|
|
||||
| Left | 1.76 | **1.35** | **1.3x** ✓ | 0 B/op | 0 B/op |
|
||||
| Right | 1.38 | 1.43 | 1.0x | 0 B/op | 0 B/op |
|
||||
| Of | 1.68 | **1.22** | **1.4x** ✓ | 0 B/op | 0 B/op |
|
||||
|
||||
**Analysis:** Both packages perform extremely well with **zero heap allocations**. Idiomatic has a slight edge on Left and Of.
|
||||
|
||||
**Important Clarification: Neither Package Escapes to Heap**
|
||||
|
||||
A common misconception is that struct-based Either escapes to heap while tuples stay on stack. The benchmarks prove this is FALSE:
|
||||
|
||||
```go
|
||||
// Either package - NO heap allocation
|
||||
type Either[E, A any] struct {
|
||||
r A // 8 bytes
|
||||
l E // 8 bytes
|
||||
isLeft bool // 1 byte + 7 padding
|
||||
} // Total: 24 bytes
|
||||
|
||||
func Of[E, A any](value A) Either[E, A] {
|
||||
return Right[E](value) // Returns 24-byte struct BY VALUE
|
||||
}
|
||||
|
||||
// Benchmark result: 0 B/op, 0 allocs/op ✓
|
||||
```
|
||||
|
||||
**Why Either doesn't escape:**
|
||||
1. **Small struct** - At 24 bytes, it's below Go's escape threshold (~64 bytes)
|
||||
2. **Return by value** - Go returns small structs on the stack
|
||||
3. **Inlining** - The `//go:inline` directive eliminates function overhead
|
||||
4. **No pointers** - No pointer escapes in normal usage
|
||||
|
||||
**Idiomatic package:**
|
||||
```go
|
||||
// Returns native tuple - always stack allocated
|
||||
func Right[A any](a A) (A, error) {
|
||||
return a, nil // 16 bytes total (8 + 8)
|
||||
}
|
||||
|
||||
// Benchmark result: 0 B/op, 0 allocs/op ✓
|
||||
```
|
||||
|
||||
**Both achieve zero allocations** - the performance difference comes from other factors like function composition overhead, not from constructor allocations.
|
||||
|
||||
### Predicate Operations
|
||||
|
||||
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|
||||
|-----------|----------------|-------------------|---------|---------------|-------------|
|
||||
| IsLeft | 1.45 | **1.35** | **1.1x** ✓ | 0 B/op | 0 B/op |
|
||||
| IsRight | 1.47 | 1.51 | 1.0x | 0 B/op | 0 B/op |
|
||||
|
||||
**Analysis:** Virtually identical performance. The optimizations brought them to parity.
|
||||
|
||||
### Fold Operations
|
||||
|
||||
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|
||||
|-----------|----------------|-------------------|---------|---------------|-------------|
|
||||
| MonadFold (Right) | 2.71 | - | - | 0 B/op | - |
|
||||
| MonadFold (Left) | 2.26 | - | - | 0 B/op | - |
|
||||
| Fold (Right) | 4.03 | **2.75** | **1.5x** ✓ | 0 B/op | 0 B/op |
|
||||
| Fold (Left) | 3.69 | **2.40** | **1.5x** ✓ | 0 B/op | 0 B/op |
|
||||
|
||||
**Analysis:** Idiomatic package is 1.5x faster for curried Fold operations.
|
||||
|
||||
### Unwrap Operations
|
||||
|
||||
| Operation | Either (ns/op) | Idiomatic (ns/op) | Note |
|
||||
|-----------|----------------|-------------------|------|
|
||||
| Unwrap (Right) | 1.27 | N/A | Either-specific |
|
||||
| Unwrap (Left) | 1.24 | N/A | Either-specific |
|
||||
| UnwrapError (Right) | 1.27 | N/A | Either-specific |
|
||||
| UnwrapError (Left) | 1.27 | N/A | Either-specific |
|
||||
| ToError (Right) | N/A | 1.40 | Idiomatic-specific |
|
||||
| ToError (Left) | N/A | 1.84 | Idiomatic-specific |
|
||||
|
||||
**Analysis:** Both provide fast unwrapping. Idiomatic's tuple return is naturally unwrapped.
|
||||
|
||||
### Map Operations
|
||||
|
||||
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|
||||
|-----------|----------------|-------------------|---------|---------------|-------------|
|
||||
| MonadMap (Right) | 2.96 | - | - | 0 B/op | - |
|
||||
| MonadMap (Left) | 1.99 | - | - | 0 B/op | - |
|
||||
| Map (Right) | 5.13 | **4.34** | **1.2x** ✓ | 0 B/op | 0 B/op |
|
||||
| Map (Left) | 4.19 | **2.48** | **1.7x** ✓ | 0 B/op | 0 B/op |
|
||||
| MapLeft (Right) | 3.93 | **2.22** | **1.8x** ✓ | 0 B/op | 0 B/op |
|
||||
| MapLeft (Left) | 7.22 | **3.51** | **2.1x** ✓ | 0 B/op | 0 B/op |
|
||||
|
||||
**Analysis:** Idiomatic is consistently faster across all Map variants, especially for error path (Left).
|
||||
|
||||
### BiMap Operations
|
||||
|
||||
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|
||||
|-----------|----------------|-------------------|---------|---------------|-------------|
|
||||
| BiMap (Right) | 16.79 | **3.82** | **4.4x** ✓ | 0 B/op | 0 B/op |
|
||||
| BiMap (Left) | 11.47 | **3.47** | **3.3x** ✓ | 0 B/op | 0 B/op |
|
||||
|
||||
**Analysis:** Idiomatic package shows significant advantage for BiMap operations (3-4x faster).
|
||||
|
||||
### Chain (Monadic Bind) Operations
|
||||
|
||||
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|
||||
|-----------|----------------|-------------------|---------|---------------|-------------|
|
||||
| MonadChain (Right) | 2.89 | - | - | 0 B/op | - |
|
||||
| MonadChain (Left) | 2.03 | - | - | 0 B/op | - |
|
||||
| Chain (Right) | 5.44 | **2.34** | **2.3x** ✓ | 0 B/op | 0 B/op |
|
||||
| Chain (Left) | 4.44 | **2.53** | **1.8x** ✓ | 0 B/op | 0 B/op |
|
||||
| ChainFirst (Right) | 87.62 | **2.71** | **32.4x** ✓✓✓ | 72 B, 3 allocs | 0 B, 0 allocs |
|
||||
| ChainFirst (Left) | 3.94 | **2.48** | **1.6x** ✓ | 0 B/op | 0 B/op |
|
||||
|
||||
**Analysis:**
|
||||
- Idiomatic is 2x faster for standard Chain operations
|
||||
- **ChainFirst shows the most dramatic difference**: 32.4x faster with zero allocations vs 72 bytes!
|
||||
|
||||
### Flatten Operations
|
||||
|
||||
| Operation | Either (ns/op) | Idiomatic (ns/op) | Note |
|
||||
|-----------|----------------|-------------------|------|
|
||||
| Flatten (Right) | 8.73 | N/A | Either-specific nested structure |
|
||||
| Flatten (Left) | 8.86 | N/A | Either-specific nested structure |
|
||||
|
||||
**Analysis:** Flatten is specific to Either's nested structure handling.
|
||||
|
||||
### Applicative Operations
|
||||
|
||||
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|
||||
|-----------|----------------|-------------------|---------|---------------|-------------|
|
||||
| MonadAp (RR) | 3.81 | - | - | 0 B/op | - |
|
||||
| MonadAp (RL) | 3.07 | - | - | 0 B/op | - |
|
||||
| MonadAp (LR) | 3.08 | - | - | 0 B/op | - |
|
||||
| Ap (RR) | 6.99 | - | - | 0 B/op | - |
|
||||
|
||||
**Analysis:** MonadAp is fast in Either. Idiomatic package doesn't expose direct Ap benchmarks.
|
||||
|
||||
### Alternative Operations
|
||||
|
||||
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|
||||
|-----------|----------------|-------------------|---------|---------------|-------------|
|
||||
| Alt (RR) | 5.72 | **2.40** | **2.4x** ✓ | 0 B/op | 0 B/op |
|
||||
| Alt (LR) | 4.89 | **2.39** | **2.0x** ✓ | 0 B/op | 0 B/op |
|
||||
| OrElse (Right) | 5.28 | **2.40** | **2.2x** ✓ | 0 B/op | 0 B/op |
|
||||
| OrElse (Left) | 3.99 | **2.42** | **1.6x** ✓ | 0 B/op | 0 B/op |
|
||||
|
||||
**Analysis:** Idiomatic package is consistently 2x faster for alternative operations.
|
||||
|
||||
### GetOrElse Operations
|
||||
|
||||
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|
||||
|-----------|----------------|-------------------|---------|---------------|-------------|
|
||||
| GetOrElse (Right) | 9.01 | **1.49** | **6.1x** ✓✓ | 0 B/op | 0 B/op |
|
||||
| GetOrElse (Left) | 6.35 | **2.08** | **3.1x** ✓✓ | 0 B/op | 0 B/op |
|
||||
|
||||
**Analysis:** Idiomatic package shows dramatic advantage for value extraction (3-6x faster).
|
||||
|
||||
### TryCatch Operations
|
||||
|
||||
| Operation | Either (ns/op) | Idiomatic (ns/op) | Note |
|
||||
|-----------|----------------|-------------------|------|
|
||||
| TryCatch (Success) | 2.39 | N/A | Either-specific |
|
||||
| TryCatch (Error) | 3.40 | N/A | Either-specific |
|
||||
| TryCatchError (Success) | 3.32 | N/A | Either-specific |
|
||||
| TryCatchError (Error) | 6.44 | N/A | Either-specific |
|
||||
|
||||
**Analysis:** TryCatch/TryCatchError are Either-specific for wrapping (value, error) tuples.
|
||||
|
||||
### Other Operations
|
||||
|
||||
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|
||||
|-----------|----------------|-------------------|---------|---------------|-------------|
|
||||
| Swap (Right) | 2.30 | - | - | 0 B/op | - |
|
||||
| Swap (Left) | 3.05 | - | - | 0 B/op | - |
|
||||
| MapTo (Right) | - | 1.60 | - | - | 0 B/op |
|
||||
| MapTo (Left) | - | 1.73 | - | - | 0 B/op |
|
||||
| ChainTo (Right) | - | 2.66 | - | - | 0 B/op |
|
||||
| ChainTo (Left) | - | 2.85 | - | - | 0 B/op |
|
||||
| Reduce (Right) | - | 2.34 | - | - | 0 B/op |
|
||||
| Reduce (Left) | - | 1.40 | - | - | 0 B/op |
|
||||
| Flap (Right) | - | 3.86 | - | - | 0 B/op |
|
||||
| Flap (Left) | - | 2.58 | - | - | 0 B/op |
|
||||
|
||||
### FromPredicate Operations
|
||||
|
||||
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|
||||
|-----------|----------------|-------------------|---------|---------------|-------------|
|
||||
| FromPredicate (Pass) | - | 3.38 | - | - | 0 B/op |
|
||||
| FromPredicate (Fail) | - | 5.03 | - | - | 0 B/op |
|
||||
|
||||
**Analysis:** FromPredicate in idiomatic shows good performance for validation patterns.
|
||||
|
||||
### Option Conversion
|
||||
|
||||
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|
||||
|-----------|----------------|-------------------|---------|---------------|-------------|
|
||||
| ToOption (Right) | - | 1.17 | - | - | 0 B/op |
|
||||
| ToOption (Left) | - | 1.21 | - | - | 0 B/op |
|
||||
| FromOption (Some) | - | 2.68 | - | - | 0 B/op |
|
||||
| FromOption (None) | - | 3.72 | - | - | 0 B/op |
|
||||
|
||||
**Analysis:** Very fast conversion between Result and Option in idiomatic package.
|
||||
|
||||
## Pipeline Benchmarks
|
||||
|
||||
These benchmarks measure realistic composition scenarios using F.Pipe.
|
||||
|
||||
### Simple Map Pipeline
|
||||
|
||||
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|
||||
|-----------|----------------|-------------------|---------|---------------|-------------|
|
||||
| Pipeline Map (Right) | 112.7 | **46.5** | **2.4x** ✓ | 72 B, 3 allocs | 48 B, 2 allocs |
|
||||
| Pipeline Map (Left) | 116.8 | **47.2** | **2.5x** ✓ | 72 B, 3 allocs | 48 B, 2 allocs |
|
||||
|
||||
### Chain Pipeline
|
||||
|
||||
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|
||||
|-----------|----------------|-------------------|---------|---------------|-------------|
|
||||
| Pipeline Chain (Right) | 74.4 | **26.1** | **2.9x** ✓ | 48 B, 2 allocs | 24 B, 1 allocs |
|
||||
| Pipeline Chain (Left) | 86.4 | **25.7** | **3.4x** ✓ | 48 B, 2 allocs | 24 B, 1 allocs |
|
||||
|
||||
### Complex Pipeline (Map → Chain → Map)
|
||||
|
||||
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|
||||
|-----------|----------------|-------------------|---------|---------------|-------------|
|
||||
| Complex (Right) | 279.8 | **116.3** | **2.4x** ✓ | 192 B, 8 allocs | 120 B, 5 allocs |
|
||||
| Complex (Left) | 288.1 | **115.8** | **2.5x** ✓ | 192 B, 8 allocs | 120 B, 5 allocs |
|
||||
|
||||
**Analysis:**
|
||||
- Idiomatic package shows **2-3.4x speedup** for realistic pipelines
|
||||
- Significantly fewer allocations in all pipeline scenarios
|
||||
- The gap widens as pipelines become more complex
|
||||
|
||||
## Array/Collection Operations
|
||||
|
||||
### TraverseArray
|
||||
|
||||
| Operation | Either (ns/op) | Idiomatic (ns/op) | Note |
|
||||
|-----------|----------------|-------------------|------|
|
||||
| TraverseArray (Success) | - | 32.3 | 48 B, 1 alloc |
|
||||
| TraverseArray (Error) | - | 28.3 | 48 B, 1 alloc |
|
||||
|
||||
**Analysis:** Idiomatic package provides efficient array traversal with minimal allocations.
|
||||
|
||||
## Validation (ApV)
|
||||
|
||||
### ApV Operations
|
||||
|
||||
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|
||||
|-----------|----------------|-------------------|---------|---------------|-------------|
|
||||
| ApV (BothRight) | - | 1.17 | - | - | 0 B/op |
|
||||
| ApV (BothLeft) | - | 141.5 | - | - | 48 B, 2 allocs |
|
||||
|
||||
**Analysis:** Idiomatic's validation applicative shows fast success path, with allocations only when accumulating errors.
|
||||
|
||||
## String Formatting
|
||||
|
||||
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|
||||
|-----------|----------------|-------------------|---------|---------------|-------------|
|
||||
| String/ToString (Right) | 139.9 | **81.8** | **1.7x** ✓ | 16 B, 1 alloc | 16 B, 1 alloc |
|
||||
| String/ToString (Left) | 161.6 | **72.7** | **2.2x** ✓ | 48 B, 1 alloc | 24 B, 1 alloc |
|
||||
|
||||
**Analysis:** Idiomatic package formats strings faster with fewer allocations for Left values.
|
||||
|
||||
## Do-Notation
|
||||
|
||||
| Operation | Either (ns/op) | Idiomatic (ns/op) | Note |
|
||||
|-----------|----------------|-------------------|------|
|
||||
| Do | 2.03 | - | Either-specific |
|
||||
| Bind | 153.4 | - | 96 B, 4 allocs |
|
||||
| Let | 33.5 | - | 16 B, 1 alloc |
|
||||
|
||||
**Analysis:** Do-notation is specific to Either package for monadic composition patterns.
|
||||
|
||||
## Summary Statistics
|
||||
|
||||
### Simple Operations (< 10 ns/op)
|
||||
|
||||
**Either Package:**
|
||||
- Count: 24 operations
|
||||
- Average: 3.2 ns/op
|
||||
- Range: 1.24 - 9.01 ns/op
|
||||
|
||||
**Idiomatic Package:**
|
||||
- Count: 36 operations
|
||||
- Average: 2.1 ns/op
|
||||
- Range: 1.17 - 5.03 ns/op
|
||||
|
||||
**Winner:** Idiomatic (1.5x faster average)
|
||||
|
||||
### Complex Operations (Pipelines, allocations)
|
||||
|
||||
**Either Package:**
|
||||
- Pipeline Map: 112.7 ns/op (72 B, 3 allocs)
|
||||
- Pipeline Chain: 74.4 ns/op (48 B, 2 allocs)
|
||||
- Complex: 279.8 ns/op (192 B, 8 allocs)
|
||||
- ChainFirst: 87.6 ns/op (72 B, 3 allocs)
|
||||
|
||||
**Idiomatic Package:**
|
||||
- Pipeline Map: 46.5 ns/op (48 B, 2 allocs)
|
||||
- Pipeline Chain: 26.1 ns/op (24 B, 1 allocs)
|
||||
- Complex: 116.3 ns/op (120 B, 5 allocs)
|
||||
- ChainFirst: 2.71 ns/op (0 B, 0 allocs)
|
||||
|
||||
**Winner:** Idiomatic (2-32x faster, significantly fewer allocations)
|
||||
|
||||
### Allocation Analysis
|
||||
|
||||
**Either Package:**
|
||||
- Zero-allocation operations: Most simple operations
|
||||
- Operations with allocations: Pipelines, Bind, Do-notation, ChainFirst
|
||||
|
||||
**Idiomatic Package:**
|
||||
- Zero-allocation operations: Almost all operations except pipelines and validation
|
||||
- Significantly fewer allocations in pipeline scenarios
|
||||
- ChainFirst: **Zero allocations** (vs 72 B in Either)
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
### Where Either Package Excels
|
||||
|
||||
1. **Comparable to Idiomatic**: After optimizations, Either matches Idiomatic for constructors and predicates
|
||||
2. **Feature Richness**: More operations (Do-notation, Bind, Let, Flatten, Swap)
|
||||
3. **Type Flexibility**: Full Either[E, A] with custom error types
|
||||
|
||||
### Where Idiomatic Package Excels
|
||||
|
||||
1. **Core Operations**: 1.2-2.3x faster for Map, Chain, Fold
|
||||
2. **Complex Operations**: 32x faster for ChainFirst
|
||||
3. **Pipelines**: 2-3.4x faster with fewer allocations
|
||||
4. **Extraction**: 3-6x faster for GetOrElse
|
||||
5. **Alternative**: 2x faster for Alt/OrElse
|
||||
6. **BiMap**: 3-4x faster
|
||||
7. **Consistency**: More predictable performance profile
|
||||
|
||||
## Real-World Performance Impact
|
||||
|
||||
### Hot Path Example (1 million operations)
|
||||
|
||||
```go
|
||||
// Map operation (very common)
|
||||
// Either: 5.13 ns/op × 1M = 5.13 ms
|
||||
// Idiomatic: 4.34 ns/op × 1M = 4.34 ms
|
||||
// Savings: 0.79 ms per million operations
|
||||
|
||||
// Chain operation (common in pipelines)
|
||||
// Either: 5.44 ns/op × 1M = 5.44 ms
|
||||
// Idiomatic: 2.34 ns/op × 1M = 2.34 ms
|
||||
// Savings: 3.10 ms per million operations
|
||||
|
||||
// Pipeline Complex (realistic composition)
|
||||
// Either: 279.8 ns/op × 1M = 279.8 ms
|
||||
// Idiomatic: 116.3 ns/op × 1M = 116.3 ms
|
||||
// Savings: 163.5 ms per million operations
|
||||
```
|
||||
|
||||
### Memory Impact
|
||||
|
||||
For 1 million ChainFirst operations:
|
||||
- Either: 72 MB allocated
|
||||
- Idiomatic: 0 MB allocated
|
||||
- **Savings: 72 MB + reduced GC pressure**
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Use Idiomatic Package When:
|
||||
|
||||
1. **Performance is Critical**
|
||||
- Hot paths in your application
|
||||
- High-throughput services (>10k req/s)
|
||||
- Complex operation chains
|
||||
- Memory-constrained environments
|
||||
|
||||
2. **Natural Go Integration**
|
||||
- Working with stdlib (value, error) patterns
|
||||
- Team familiar with Go idioms
|
||||
- Simple migration from existing code
|
||||
- Want zero-cost abstractions
|
||||
|
||||
3. **Pipeline-Heavy Code**
|
||||
- 2-3.4x faster pipelines
|
||||
- Significantly fewer allocations
|
||||
- Better CPU cache utilization
|
||||
|
||||
### Use Either Package When:
|
||||
|
||||
1. **Feature Requirements**
|
||||
- Need custom error types (Either[E, A])
|
||||
- Using Do-notation for complex compositions
|
||||
- Need Flatten, Swap, or other Either-specific operations
|
||||
- Porting from FP languages (Scala, Haskell)
|
||||
|
||||
2. **Type Safety Over Performance**
|
||||
- Explicit Either semantics
|
||||
- Algebraic data type guarantees
|
||||
- Teaching/learning FP concepts
|
||||
|
||||
3. **Moderate Performance Needs**
|
||||
- After optimizations, Either is quite fast
|
||||
- Difference matters only at high scale
|
||||
- Code clarity > micro-optimizations
|
||||
|
||||
### Hybrid Approach
|
||||
|
||||
```go
|
||||
// Use Either for complex type safety
|
||||
import "github.com/IBM/fp-go/v2/either"
|
||||
type ValidationError struct { Field, Message string }
|
||||
validated := either.Either[ValidationError, Input]{...}
|
||||
|
||||
// Convert to Idiomatic for hot path
|
||||
import "github.com/IBM/fp-go/v2/idiomatic/result"
|
||||
value, err := either.UnwrapError(either.MapLeft(toError)(validated))
|
||||
processed, err := result.Chain(hotPathProcessing)(value, err)
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
After optimizations to the Either package:
|
||||
|
||||
1. **Both packages achieve zero heap allocations for constructors** - The Either struct (24 bytes) does NOT escape to heap
|
||||
2. **Simple operations** are now **comparable** between both packages (~1-2 ns/op, 0 B/op)
|
||||
3. **Core transformations** favor Idiomatic by **1.2-2.3x**
|
||||
4. **Complex operations** heavily favor Idiomatic by **2-32x**
|
||||
5. **Memory efficiency** strongly favors Idiomatic (especially ChainFirst: 72 B → 0 B)
|
||||
6. **Real-world pipelines** show **2-3.4x speedup** with Idiomatic
|
||||
|
||||
### Key Insight: No Heap Escape Myth
|
||||
|
||||
A critical finding: **Both packages avoid heap allocations for simple operations.** The Either struct is small enough (24 bytes) that Go returns it by value on the stack, not the heap. The `0 B/op, 0 allocs/op` benchmarks confirm this.
|
||||
|
||||
The performance differences come from:
|
||||
- **Function composition overhead** in complex operations
|
||||
- **Currying and closure creation** in pipelines
|
||||
- **Tuple simplicity** vs struct field access
|
||||
|
||||
Not from constructor allocations—both are equally efficient there.
|
||||
|
||||
### Final Verdict
|
||||
|
||||
The idiomatic package provides a compelling performance advantage for production workloads while maintaining zero-cost functional programming abstractions. The Either package remains excellent for type safety, feature richness, and scenarios where explicit Either[E, A] semantics are valuable.
|
||||
|
||||
**Bottom Line:**
|
||||
- For **high-performance Go services**: idiomatic package is the clear winner (1.2-32x faster)
|
||||
- For **type-safe, feature-rich FP**: Either package is excellent (comparable simple ops, more features)
|
||||
- **Both avoid heap allocations** for constructors—choose based on your performance vs features trade-off
|
||||
344
v2/CHAINING_PERFORMANCE_ANALYSIS.md
Normal file
344
v2/CHAINING_PERFORMANCE_ANALYSIS.md
Normal file
@@ -0,0 +1,344 @@
|
||||
# Deep Chaining Performance Analysis
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The **only remaining performance gap** between `v2/option` and `idiomatic/option` is in **deep chaining operations** (multiple sequential transformations). This document demonstrates the problem, explains the root cause, and provides recommendations.
|
||||
|
||||
## Benchmark Results
|
||||
|
||||
### v2/option (Struct-based)
|
||||
```
|
||||
BenchmarkChain_3Steps 8.17 ns/op 0 allocs
|
||||
BenchmarkChain_5Steps 16.57 ns/op 0 allocs
|
||||
BenchmarkChain_10Steps 47.01 ns/op 0 allocs
|
||||
BenchmarkMap_5Steps 0.28 ns/op 0 allocs ⚡
|
||||
```
|
||||
|
||||
### idiomatic/option (Tuple-based)
|
||||
```
|
||||
BenchmarkChain_3Steps 0.22 ns/op 0 allocs ⚡
|
||||
BenchmarkChain_5Steps 0.22 ns/op 0 allocs ⚡
|
||||
BenchmarkChain_10Steps 0.21 ns/op 0 allocs ⚡
|
||||
BenchmarkMap_5Steps 0.22 ns/op 0 allocs ⚡
|
||||
```
|
||||
|
||||
### Performance Comparison
|
||||
|
||||
| Steps | v2/option | idiomatic/option | Slowdown |
|
||||
|-------|-----------|------------------|----------|
|
||||
| 3 | 8.17 ns | 0.22 ns | **37x slower** |
|
||||
| 5 | 16.57 ns | 0.22 ns | **75x slower** |
|
||||
| 10 | 47.01 ns | 0.21 ns | **224x slower** |
|
||||
|
||||
**Key Finding**: The performance gap **increases linearly** with chain depth!
|
||||
|
||||
---
|
||||
|
||||
## Visual Example: The Problem
|
||||
|
||||
### Scenario: Processing User Input
|
||||
|
||||
```go
|
||||
// Process user input through multiple validation steps
|
||||
input := "42"
|
||||
|
||||
// v2/option - Nested MonadChain
|
||||
result := MonadChain(
|
||||
MonadChain(
|
||||
MonadChain(
|
||||
Some(input),
|
||||
validateNotEmpty, // Step 1
|
||||
),
|
||||
parseToInt, // Step 2
|
||||
),
|
||||
validateRange, // Step 3
|
||||
)
|
||||
```
|
||||
|
||||
### What Happens Under the Hood
|
||||
|
||||
#### v2/option (Struct Construction Overhead)
|
||||
|
||||
```go
|
||||
// Step 0: Initial value
|
||||
Some(input)
|
||||
// Creates: Option[string]{value: "42", isSome: true}
|
||||
// Memory: HEAP allocation
|
||||
|
||||
// Step 1: Validate not empty
|
||||
MonadChain(opt, validateNotEmpty)
|
||||
// Input: Option[string]{value: "42", isSome: true} ← Read from heap
|
||||
// Output: Option[string]{value: "42", isSome: true} ← NEW heap allocation
|
||||
// Memory: 2 heap allocations
|
||||
|
||||
// Step 2: Parse to int
|
||||
MonadChain(opt, parseToInt)
|
||||
// Input: Option[string]{value: "42", isSome: true} ← Read from heap
|
||||
// Output: Option[int]{value: 42, isSome: true} ← NEW heap allocation
|
||||
// Memory: 3 heap allocations
|
||||
|
||||
// Step 3: Validate range
|
||||
MonadChain(opt, validateRange)
|
||||
// Input: Option[int]{value: 42, isSome: true} ← Read from heap
|
||||
// Output: Option[int]{value: 42, isSome: true} ← NEW heap allocation
|
||||
// Memory: 4 heap allocations TOTAL
|
||||
|
||||
// Each step:
|
||||
// 1. Reads Option struct from memory
|
||||
// 2. Checks isSome field
|
||||
// 3. Calls function
|
||||
// 4. Creates NEW Option struct
|
||||
// 5. Writes to memory
|
||||
```
|
||||
|
||||
#### idiomatic/option (Zero Allocation)
|
||||
|
||||
```go
|
||||
// Step 0: Initial value
|
||||
s, ok := Some(input)
|
||||
// Creates: ("42", true)
|
||||
// Memory: STACK only (registers)
|
||||
|
||||
// Step 1: Validate not empty
|
||||
v1, ok1 := Chain(validateNotEmpty)(s, ok)
|
||||
// Input: ("42", true) ← Values in registers
|
||||
// Output: ("42", true) ← Values in registers
|
||||
// Memory: ZERO allocations
|
||||
|
||||
// Step 2: Parse to int
|
||||
v2, ok2 := Chain(parseToInt)(v1, ok1)
|
||||
// Input: ("42", true) ← Values in registers
|
||||
// Output: (42, true) ← Values in registers
|
||||
// Memory: ZERO allocations
|
||||
|
||||
// Step 3: Validate range
|
||||
v3, ok3 := Chain(validateRange)(v2, ok2)
|
||||
// Input: (42, true) ← Values in registers
|
||||
// Output: (42, true) ← Values in registers
|
||||
// Memory: ZERO allocations TOTAL
|
||||
|
||||
// Each step:
|
||||
// 1. Reads values from registers (no memory access!)
|
||||
// 2. Checks bool flag
|
||||
// 3. Calls function
|
||||
// 4. Returns new tuple (stays in registers)
|
||||
// 5. Compiler optimizes everything away!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Assembly-Level Difference
|
||||
|
||||
### v2/option - Struct Overhead
|
||||
|
||||
```asm
|
||||
; Every chain step does:
|
||||
MOV RAX, [heap_ptr] ; Load struct from heap
|
||||
TEST BYTE [RAX+8], 1 ; Check isSome field
|
||||
JZ none_case ; Branch if None
|
||||
MOV RDI, [RAX] ; Load value from struct
|
||||
CALL transform_func ; Call the function
|
||||
CALL malloc ; Allocate new struct ⚠️
|
||||
MOV [new_ptr], result ; Store result
|
||||
MOV [new_ptr+8], 1 ; Set isSome = true
|
||||
```
|
||||
|
||||
### idiomatic/option - Optimized Away
|
||||
|
||||
```asm
|
||||
; All steps compiled to:
|
||||
MOV EAX, 42 ; The final result!
|
||||
; Everything else optimized away! ⚡
|
||||
```
|
||||
|
||||
**Compiler insight**: With tuples, the Go compiler can:
|
||||
1. **Inline everything** - No function call overhead
|
||||
2. **Eliminate branches** - Constant propagation removes `if ok` checks
|
||||
3. **Use registers only** - Values never touch memory
|
||||
4. **Dead code elimination** - Removes unnecessary operations
|
||||
|
||||
---
|
||||
|
||||
## Real-World Example with Timings
|
||||
|
||||
### Example: User Registration Validation Chain
|
||||
|
||||
```go
|
||||
// Validate: email → trim → lowercase → check format → check uniqueness
|
||||
```
|
||||
|
||||
#### v2/option Performance
|
||||
|
||||
```go
|
||||
func ValidateEmail_v2(email string) Option[string] {
|
||||
return MonadChain(
|
||||
MonadChain(
|
||||
MonadChain(
|
||||
MonadChain(
|
||||
Some(email),
|
||||
trimWhitespace, // ~2 ns
|
||||
),
|
||||
toLowerCase, // ~2 ns
|
||||
),
|
||||
validateFormat, // ~2 ns
|
||||
),
|
||||
checkUniqueness, // ~2 ns
|
||||
)
|
||||
}
|
||||
// Total: ~8-16 ns (matches our 5-step benchmark: 16.57 ns)
|
||||
```
|
||||
|
||||
#### idiomatic/option Performance
|
||||
|
||||
```go
|
||||
func ValidateEmail_idiomatic(email string) (string, bool) {
|
||||
v1, ok1 := Chain(trimWhitespace)(email, true)
|
||||
v2, ok2 := Chain(toLowerCase)(v1, ok1)
|
||||
v3, ok3 := Chain(validateFormat)(v2, ok2)
|
||||
return Chain(checkUniqueness)(v3, ok3)
|
||||
}
|
||||
// Total: ~0.22 ns (entire chain optimized to single operation!)
|
||||
```
|
||||
|
||||
**Impact**: For 1 million validations:
|
||||
- v2/option: 16.57 ms
|
||||
- idiomatic/option: 0.22 ms
|
||||
- **Difference: 75x faster = saved 16.35 ms**
|
||||
|
||||
---
|
||||
|
||||
## Why Map is Fast in v2/option
|
||||
|
||||
Interestingly, `Map` (pure transformations) is **much faster** than `Chain`:
|
||||
|
||||
```
|
||||
v2/option:
|
||||
- BenchmarkChain_5Steps: 16.57 ns
|
||||
- BenchmarkMap_5Steps: 0.28 ns ← 59x FASTER!
|
||||
```
|
||||
|
||||
**Reason**: Map transformations can be **inlined and fused** by the compiler:
|
||||
|
||||
```go
|
||||
// This:
|
||||
Map(f5)(Map(f4)(Map(f3)(Map(f2)(Map(f1)(opt)))))
|
||||
|
||||
// Becomes (after compiler optimization):
|
||||
Some(f5(f4(f3(f2(f1(value)))))) // Single struct construction!
|
||||
|
||||
// While Chain cannot be optimized the same way:
|
||||
MonadChain(MonadChain(...)) // Must construct at each step
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When Does This Matter?
|
||||
|
||||
### ⚠️ **Rarely Critical** (99% of use cases)
|
||||
|
||||
Even 10-step chains only cost **47 nanoseconds**. For context:
|
||||
- Database query: **~1,000,000 ns** (1 ms)
|
||||
- HTTP request: **~10,000,000 ns** (10 ms)
|
||||
- File I/O: **~100,000 ns** (0.1 ms)
|
||||
|
||||
**The 47 ns overhead is negligible compared to real I/O operations.**
|
||||
|
||||
### ⚡ **Can Matter** (High-throughput scenarios)
|
||||
|
||||
1. **In-memory data processing pipelines**
|
||||
```go
|
||||
// Processing 10 million records with 5-step validation
|
||||
v2/option: 165 ms
|
||||
idiomatic/option: 2 ms
|
||||
Difference: 163 ms saved ⚡
|
||||
```
|
||||
|
||||
2. **Real-time stream processing**
|
||||
- Processing 100k events/second with chained transformations
|
||||
- 16.57 ns × 100,000 = 1.66 ms vs 0.22 ns × 100,000 = 0.022 ms
|
||||
- Can affect throughput for high-frequency trading, gaming, etc.
|
||||
|
||||
3. **Tight inner loops with chained logic**
|
||||
```go
|
||||
for i := 0; i < 1_000_000; i++ {
|
||||
result := Chain(f1).Chain(f2).Chain(f3).Chain(f4)(data[i])
|
||||
}
|
||||
// v2/option: 16 ms
|
||||
// idiomatic: 0.22 ms
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Root Cause Summary
|
||||
|
||||
| Aspect | v2/option | idiomatic/option | Why? |
|
||||
|--------|-----------|------------------|------|
|
||||
| **Intermediate values** | `Option[T]` struct | `(T, bool)` tuple | Struct requires memory, tuple can use registers |
|
||||
| **Memory allocation** | 1 per step | 0 total | Heap vs stack |
|
||||
| **Compiler optimization** | Limited | Aggressive | Structs block inlining |
|
||||
| **Cache impact** | Heap reads | Register-only | Memory bandwidth saved |
|
||||
| **Branch prediction** | Struct checks | Optimized away | Compiler removes branches |
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
### ✅ **Use v2/option When:**
|
||||
- I/O-bound operations (database, network, files)
|
||||
- User-facing applications (latency dominated by I/O)
|
||||
- Need JSON marshaling, TryCatch, SequenceArray
|
||||
- Chain depth < 5 steps (overhead < 20 ns - negligible)
|
||||
- Code clarity > microsecond performance
|
||||
|
||||
### ✅ **Use idiomatic/option When:**
|
||||
- CPU-bound data processing
|
||||
- High-throughput stream processing
|
||||
- Tight inner loops with chaining
|
||||
- In-memory analytics
|
||||
- Performance-critical paths
|
||||
- Chain depth > 5 steps
|
||||
|
||||
### ✅ **Mitigation for v2/option:**
|
||||
|
||||
If you need v2/option but want better chain performance:
|
||||
|
||||
1. **Use Map instead of Chain** when possible:
|
||||
```go
|
||||
// Bad (16.57 ns):
|
||||
MonadChain(MonadChain(MonadChain(opt, f1), f2), f3)
|
||||
|
||||
// Good (0.28 ns):
|
||||
Map(f3)(Map(f2)(Map(f1)(opt)))
|
||||
```
|
||||
|
||||
2. **Batch operations**:
|
||||
```go
|
||||
// Instead of chaining many steps:
|
||||
validate := func(x T) Option[T] {
|
||||
// Combine multiple checks in one function
|
||||
if check1(x) && check2(x) && check3(x) {
|
||||
return Some(transform(x))
|
||||
}
|
||||
return None[T]()
|
||||
}
|
||||
```
|
||||
|
||||
3. **Profile first**:
|
||||
- Only optimize hot paths
|
||||
- 47 ns is often acceptable
|
||||
- Don't premature optimize
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
**The deep chaining performance gap is:**
|
||||
- ✅ **Real and measurable** (37-224x slower)
|
||||
- ✅ **Well understood** (struct construction overhead)
|
||||
- ⚠️ **Rarely critical** (nanosecond differences usually don't matter)
|
||||
- ✅ **Easy to work around** (use Map, batch operations)
|
||||
- ✅ **Worth it for the API benefits** (JSON, methods, helpers)
|
||||
|
||||
**For 99% of applications, v2/option's performance is excellent.** The gap only matters in specialized high-throughput scenarios where you should probably use idiomatic/option anyway.
|
||||
|
||||
The optimizations already applied (`//go:inline`, direct field access) brought v2/option to **competitive parity** for all practical purposes. The remaining gap is a **fundamental design trade-off**, not a fixable bug.
|
||||
574
v2/DESIGN.md
Normal file
574
v2/DESIGN.md
Normal file
@@ -0,0 +1,574 @@
|
||||
# Design Decisions
|
||||
|
||||
This document explains the key design decisions and principles behind fp-go's API design.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Data Last Principle](#data-last-principle)
|
||||
- [Kleisli and Operator Types](#kleisli-and-operator-types)
|
||||
- [Monadic Operations Comparison](#monadic-operations-comparison)
|
||||
- [Type Parameter Ordering](#type-parameter-ordering)
|
||||
- [Generic Type Aliases](#generic-type-aliases)
|
||||
|
||||
## Data Last Principle
|
||||
|
||||
fp-go follows the **"data last"** principle, where the data being operated on is always the last parameter in a function. This design choice enables powerful function composition and partial application patterns.
|
||||
|
||||
### What is "Data Last"?
|
||||
|
||||
In the "data last" style, functions are structured so that:
|
||||
1. Configuration parameters come first
|
||||
2. The data to be transformed comes last
|
||||
|
||||
This is the opposite of the traditional object-oriented style where the data (receiver) comes first.
|
||||
|
||||
### Why "Data Last"?
|
||||
|
||||
The "data last" principle enables:
|
||||
|
||||
1. **Natural Currying**: Functions can be partially applied to create specialized transformations
|
||||
2. **Function Composition**: Operations can be composed before applying them to data
|
||||
3. **Point-Free Style**: Write transformations without explicitly mentioning the data
|
||||
4. **Reusability**: Create reusable transformation pipelines
|
||||
|
||||
### Examples
|
||||
|
||||
#### Basic Transformation
|
||||
|
||||
```go
|
||||
// Data last style (fp-go)
|
||||
double := array.Map(number.Mul(2))
|
||||
result := double([]int{1, 2, 3}) // [2, 4, 6]
|
||||
|
||||
// Compare with data first style (traditional)
|
||||
result := array.Map([]int{1, 2, 3}, number.Mul(2))
|
||||
```
|
||||
|
||||
#### Function Composition
|
||||
|
||||
```go
|
||||
import (
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
// Create a pipeline of transformations
|
||||
pipeline := F.Flow3(
|
||||
A.Filter(func(x int) bool { return x > 0 }), // Keep positive numbers
|
||||
A.Map(N.Mul(2)), // Double each number
|
||||
A.Reduce(func(acc, x int) int { return acc + x }, 0), // Sum them up
|
||||
)
|
||||
|
||||
// Apply the pipeline to different data
|
||||
result1 := pipeline([]int{-1, 2, 3, -4, 5}) // (2 + 3 + 5) * 2 = 20
|
||||
result2 := pipeline([]int{1, 2, 3}) // (1 + 2 + 3) * 2 = 12
|
||||
```
|
||||
|
||||
#### Partial Application
|
||||
|
||||
```go
|
||||
import (
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// Create specialized functions by partial application
|
||||
getOrZero := O.GetOrElse(func() int { return 0 })
|
||||
getOrEmpty := O.GetOrElse(func() string { return "" })
|
||||
|
||||
// Use them with different data
|
||||
value1 := getOrZero(O.Some(42)) // 42
|
||||
value2 := getOrZero(O.None[int]()) // 0
|
||||
|
||||
text1 := getOrEmpty(O.Some("hello")) // "hello"
|
||||
text2 := getOrEmpty(O.None[string]()) // ""
|
||||
```
|
||||
|
||||
#### Building Reusable Transformations
|
||||
|
||||
```go
|
||||
import (
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// Create a reusable validation pipeline
|
||||
type User struct {
|
||||
Name string
|
||||
Email string
|
||||
Age int
|
||||
}
|
||||
|
||||
validateAge := E.FromPredicate(
|
||||
func(u User) bool { return u.Age >= 18 },
|
||||
func(u User) error { return errors.New("must be 18 or older") },
|
||||
)
|
||||
|
||||
validateEmail := E.FromPredicate(
|
||||
func(u User) bool { return strings.Contains(u.Email, "@") },
|
||||
func(u User) error { return errors.New("invalid email") },
|
||||
)
|
||||
|
||||
// Compose validators
|
||||
validateUser := F.Flow2(
|
||||
validateAge,
|
||||
E.Chain(validateEmail),
|
||||
)
|
||||
|
||||
// Apply to different users
|
||||
result1 := validateUser(User{Name: "Alice", Email: "alice@example.com", Age: 25})
|
||||
result2 := validateUser(User{Name: "Bob", Email: "invalid", Age: 30})
|
||||
```
|
||||
|
||||
#### Monadic Operations
|
||||
|
||||
```go
|
||||
import (
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// Data last enables clean monadic chains
|
||||
parseAndDouble := F.Flow2(
|
||||
O.FromPredicate(func(s string) bool { return s != "" }),
|
||||
O.Chain(func(s string) O.Option[int] {
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return O.None[int]()
|
||||
}
|
||||
return O.Some(n * 2)
|
||||
}),
|
||||
)
|
||||
|
||||
result1 := parseAndDouble("21") // Some(42)
|
||||
result2 := parseAndDouble("") // None
|
||||
result3 := parseAndDouble("abc") // None
|
||||
```
|
||||
|
||||
### Monadic vs Non-Monadic Forms
|
||||
|
||||
fp-go provides two forms for most operations:
|
||||
|
||||
1. **Curried form** (data last): Returns a function that can be composed
|
||||
2. **Monadic form** (data first): Takes all parameters at once
|
||||
|
||||
```go
|
||||
// Curried form - data last, returns a function
|
||||
Map[A, B any](f func(A) B) func(Option[A]) Option[B]
|
||||
|
||||
// Monadic form - data first, direct execution
|
||||
MonadMap[A, B any](fa Option[A], f func(A) B) Option[B]
|
||||
```
|
||||
|
||||
**When to use each:**
|
||||
|
||||
- **Curried form**: When building pipelines, composing functions, or creating reusable transformations
|
||||
- **Monadic form**: When you have all parameters available and want direct execution
|
||||
|
||||
```go
|
||||
// Curried form - building a pipeline
|
||||
transform := F.Flow3(
|
||||
O.Map(strings.ToUpper),
|
||||
O.Filter(func(s string) bool { return len(s) > 3 }),
|
||||
O.GetOrElse(func() string { return "DEFAULT" }),
|
||||
)
|
||||
result := transform(O.Some("hello"))
|
||||
|
||||
// Monadic form - direct execution
|
||||
result := O.MonadMap(O.Some("hello"), strings.ToUpper)
|
||||
```
|
||||
|
||||
### Further Reading on Data-Last Pattern
|
||||
|
||||
The data-last currying pattern is well-documented in the functional programming community:
|
||||
|
||||
- [Mostly Adequate Guide - Ch. 4: Currying](https://mostly-adequate.gitbook.io/mostly-adequate-guide/ch04) - Excellent introduction with clear examples
|
||||
- [Curry and Function Composition](https://medium.com/javascript-scene/curry-and-function-composition-2c208d774983) by Eric Elliott
|
||||
- [fp-ts Issue #1238](https://github.com/gcanti/fp-ts/issues/1238) - Real-world examples of data-last refactoring
|
||||
|
||||
## Kleisli and Operator Types
|
||||
|
||||
fp-go uses consistent type aliases across all monads to make code more recognizable and composable. These types provide a common vocabulary that works across different monadic contexts.
|
||||
|
||||
### Type Definitions
|
||||
|
||||
```go
|
||||
// Kleisli arrow - a function that returns a monadic value
|
||||
type Kleisli[A, B any] = func(A) M[B]
|
||||
|
||||
// Operator - a function that transforms a monadic value
|
||||
type Operator[A, B any] = func(M[A]) M[B]
|
||||
```
|
||||
|
||||
Where `M` represents the specific monad (Option, Either, IO, etc.).
|
||||
|
||||
### Why These Types Matter
|
||||
|
||||
1. **Consistency**: The same type names appear across all monads
|
||||
2. **Recognizability**: Experienced functional programmers immediately understand the intent
|
||||
3. **Composability**: Functions with these types compose naturally
|
||||
4. **Documentation**: Type signatures clearly communicate the operation's behavior
|
||||
|
||||
### Examples Across Monads
|
||||
|
||||
#### Option Monad
|
||||
|
||||
```go
|
||||
// option/option.go
|
||||
type Kleisli[A, B any] = func(A) Option[B]
|
||||
type Operator[A, B any] = func(Option[A]) Option[B]
|
||||
|
||||
// Chain uses Kleisli
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B]
|
||||
|
||||
// Map returns an Operator
|
||||
func Map[A, B any](f func(A) B) Operator[A, B]
|
||||
```
|
||||
|
||||
#### Either Monad
|
||||
|
||||
```go
|
||||
// either/either.go
|
||||
type Kleisli[E, A, B any] = func(A) Either[E, B]
|
||||
type Operator[E, A, B any] = func(Either[E, A]) Either[E, B]
|
||||
|
||||
// Chain uses Kleisli
|
||||
func Chain[E, A, B any](f Kleisli[E, A, B]) Operator[E, A, B]
|
||||
|
||||
// Map returns an Operator
|
||||
func Map[E, A, B any](f func(A) B) Operator[E, A, B]
|
||||
```
|
||||
|
||||
#### IO Monad
|
||||
|
||||
```go
|
||||
// io/io.go
|
||||
type Kleisli[A, B any] = func(A) IO[B]
|
||||
type Operator[A, B any] = func(IO[A]) IO[B]
|
||||
|
||||
// Chain uses Kleisli
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B]
|
||||
|
||||
// Map returns an Operator
|
||||
func Map[A, B any](f func(A) B) Operator[A, B]
|
||||
```
|
||||
|
||||
#### Array (List Monad)
|
||||
|
||||
```go
|
||||
// array/array.go
|
||||
type Kleisli[A, B any] = func(A) []B
|
||||
type Operator[A, B any] = func([]A) []B
|
||||
|
||||
// Chain uses Kleisli
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B]
|
||||
|
||||
// Map returns an Operator
|
||||
func Map[A, B any](f func(A) B) Operator[A, B]
|
||||
```
|
||||
|
||||
### Pattern Recognition
|
||||
|
||||
Once you learn these patterns in one monad, you can apply them to all monads:
|
||||
|
||||
```go
|
||||
// The pattern is always the same, just the monad changes
|
||||
|
||||
// Option
|
||||
validateAge := option.Chain(func(user User) option.Option[User] {
|
||||
if user.Age >= 18 {
|
||||
return option.Some(user)
|
||||
}
|
||||
return option.None[User]()
|
||||
})
|
||||
|
||||
// Either
|
||||
validateAge := either.Chain(func(user User) either.Either[error, User] {
|
||||
if user.Age >= 18 {
|
||||
return either.Right[error](user)
|
||||
}
|
||||
return either.Left[User](errors.New("too young"))
|
||||
})
|
||||
|
||||
// IO
|
||||
validateAge := io.Chain(func(user User) io.IO[User] {
|
||||
return io.Of(user) // Always succeeds in IO
|
||||
})
|
||||
|
||||
// Array
|
||||
validateAge := array.Chain(func(user User) []User {
|
||||
if user.Age >= 18 {
|
||||
return []User{user}
|
||||
}
|
||||
return []User{} // Empty array = failure
|
||||
})
|
||||
```
|
||||
|
||||
### Composing Kleisli Arrows
|
||||
|
||||
Kleisli arrows compose naturally using monadic composition:
|
||||
|
||||
```go
|
||||
import (
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
// Define Kleisli arrows
|
||||
parseAge := func(s string) O.Option[int] {
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return O.None[int]()
|
||||
}
|
||||
return O.Some(n)
|
||||
}
|
||||
|
||||
validateAge := func(age int) O.Option[int] {
|
||||
if age >= 18 {
|
||||
return O.Some(age)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
|
||||
formatAge := func(age int) O.Option[string] {
|
||||
return O.Some(fmt.Sprintf("Age: %d", age))
|
||||
}
|
||||
|
||||
// Compose them using Flow and Chain
|
||||
pipeline := F.Flow3(
|
||||
parseAge,
|
||||
O.Chain(validateAge),
|
||||
O.Chain(formatAge),
|
||||
)
|
||||
|
||||
result := pipeline("25") // Some("Age: 25")
|
||||
result := pipeline("15") // None (too young)
|
||||
result := pipeline("abc") // None (parse error)
|
||||
```
|
||||
|
||||
### Building Reusable Operators
|
||||
|
||||
Operators can be created once and reused across your codebase:
|
||||
|
||||
```go
|
||||
import (
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
)
|
||||
|
||||
// Create reusable operators
|
||||
type ValidationError struct {
|
||||
Field string
|
||||
Message string
|
||||
}
|
||||
|
||||
// Reusable validation operators
|
||||
validateNonEmpty := E.Chain(func(s string) E.Either[ValidationError, string] {
|
||||
if s == "" {
|
||||
return E.Left[string](ValidationError{
|
||||
Field: "input",
|
||||
Message: "cannot be empty",
|
||||
})
|
||||
}
|
||||
return E.Right[ValidationError](s)
|
||||
})
|
||||
|
||||
validateEmail := E.Chain(func(s string) E.Either[ValidationError, string] {
|
||||
if !strings.Contains(s, "@") {
|
||||
return E.Left[string](ValidationError{
|
||||
Field: "email",
|
||||
Message: "invalid format",
|
||||
})
|
||||
}
|
||||
return E.Right[ValidationError](s)
|
||||
})
|
||||
|
||||
// Compose operators
|
||||
validateEmailInput := F.Flow2(
|
||||
validateNonEmpty,
|
||||
validateEmail,
|
||||
)
|
||||
|
||||
// Use across your application
|
||||
result1 := validateEmailInput(E.Right[ValidationError]("user@example.com"))
|
||||
result2 := validateEmailInput(E.Right[ValidationError](""))
|
||||
result3 := validateEmailInput(E.Right[ValidationError]("invalid"))
|
||||
```
|
||||
|
||||
### Benefits of Consistent Naming
|
||||
|
||||
1. **Cross-monad understanding**: Learn once, apply everywhere
|
||||
2. **Easier refactoring**: Changing monads requires minimal code changes
|
||||
3. **Better tooling**: IDEs can provide better suggestions
|
||||
4. **Team communication**: Shared vocabulary across the team
|
||||
5. **Library integration**: Third-party libraries follow the same patterns
|
||||
|
||||
### Identity Monad - The Simplest Case
|
||||
|
||||
The Identity monad shows these types in their simplest form:
|
||||
|
||||
```go
|
||||
// identity/doc.go
|
||||
type Operator[A, B any] = func(A) B
|
||||
|
||||
// In Identity, there's no wrapping, so:
|
||||
// - Kleisli[A, B] is just func(A) B
|
||||
// - Operator[A, B] is just func(A) B
|
||||
// They're the same because Identity adds no context
|
||||
```
|
||||
|
||||
This demonstrates that these type aliases represent fundamental functional programming concepts, not just arbitrary naming conventions.
|
||||
|
||||
|
||||
## Monadic Operations Comparison
|
||||
|
||||
fp-go's monadic operations are inspired by functional programming languages and libraries. Here's how they compare:
|
||||
|
||||
| fp-go | fp-ts | Haskell | Scala | Description |
|
||||
|-------|-------|---------|-------|-------------|
|
||||
| `Map` | `map` | `fmap` | `map` | Functor mapping - transforms the value inside a context |
|
||||
| `Chain` | `chain` | `>>=` (bind) | `flatMap` | Monadic bind - chains computations that return wrapped values |
|
||||
| `Ap` | `ap` | `<*>` | `ap` | Applicative apply - applies a wrapped function to a wrapped value |
|
||||
| `Of` | `of` | `return`/`pure` | `pure` | Lifts a pure value into a monadic context |
|
||||
| `Fold` | `fold` | `either` | `fold` | Eliminates the context by providing handlers for each case |
|
||||
| `Filter` | `filter` | `mfilter` | `filter` | Keeps values that satisfy a predicate |
|
||||
| `Flatten` | `flatten` | `join` | `flatten` | Removes one level of nesting |
|
||||
| `ChainFirst` | `chainFirst` | `>>` (then) | `tap` | Chains for side effects, keeping the original value |
|
||||
| `Alt` | `alt` | `<\|>` | `orElse` | Provides an alternative value if the first fails |
|
||||
| `GetOrElse` | `getOrElse` | `fromMaybe` | `getOrElse` | Extracts the value or provides a default |
|
||||
| `FromPredicate` | `fromPredicate` | `guard` | `filter` | Creates a monadic value based on a predicate |
|
||||
| `Sequence` | `sequence` | `sequence` | `sequence` | Transforms a collection of effects into an effect of a collection |
|
||||
| `Traverse` | `traverse` | `traverse` | `traverse` | Maps and sequences in one operation |
|
||||
| `Reduce` | `reduce` | `foldl` | `foldLeft` | Folds a structure from left to right |
|
||||
| `ReduceRight` | `reduceRight` | `foldr` | `foldRight` | Folds a structure from right to left |
|
||||
|
||||
### Key Differences from Other Languages
|
||||
|
||||
#### Naming Conventions
|
||||
|
||||
- **Go conventions**: fp-go uses PascalCase for exported functions (e.g., `Map`, `Chain`) following Go's naming conventions
|
||||
- **Type parameters first**: Non-inferrable type parameters come first (e.g., `Ap[B, E, A any]`)
|
||||
- **Monadic prefix**: Direct execution forms use the `Monad` prefix (e.g., `MonadMap`, `MonadChain`)
|
||||
|
||||
#### Type System
|
||||
|
||||
```go
|
||||
// fp-go (explicit type parameters when needed)
|
||||
result := option.Map(transform)(value)
|
||||
result := option.Map[string, int](transform)(value) // explicit when inference fails
|
||||
|
||||
// Haskell (type inference)
|
||||
result = fmap transform value
|
||||
|
||||
// Scala (type inference with method syntax)
|
||||
result = value.map(transform)
|
||||
|
||||
// fp-ts (TypeScript type inference)
|
||||
const result = pipe(value, map(transform))
|
||||
```
|
||||
|
||||
#### Currying
|
||||
|
||||
```go
|
||||
// fp-go - explicit currying with data last
|
||||
double := array.Map(number.Mul(2))
|
||||
result := double(numbers)
|
||||
|
||||
// Haskell - automatic currying
|
||||
double = fmap (*2)
|
||||
result = double numbers
|
||||
|
||||
// Scala - method syntax
|
||||
result = numbers.map(_ * 2)
|
||||
```
|
||||
|
||||
## Type Parameter Ordering
|
||||
|
||||
fp-go v2 uses a specific ordering for type parameters to maximize type inference:
|
||||
|
||||
### Rule: Non-Inferrable Parameters First
|
||||
|
||||
Type parameters that **cannot be inferred** from function arguments come first. This allows the Go compiler to infer as many types as possible.
|
||||
|
||||
```go
|
||||
// Ap - B cannot be inferred from arguments, so it comes first
|
||||
func Ap[B, E, A any](fa Either[E, A]) func(Either[E, func(A) B]) Either[E, B]
|
||||
|
||||
// Usage - only B needs to be specified
|
||||
result := either.Ap[string](value)(funcInEither)
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```go
|
||||
// Map - all types can be inferred from arguments
|
||||
func Map[E, A, B any](f func(A) B) func(Either[E, A]) Either[E, B]
|
||||
// Usage - no type parameters needed
|
||||
result := either.Map(transform)(value)
|
||||
|
||||
// Chain - all types can be inferred
|
||||
func Chain[E, A, B any](f func(A) Either[E, B]) func(Either[E, A]) Either[E, B]
|
||||
// Usage - no type parameters needed
|
||||
result := either.Chain(validator)(value)
|
||||
|
||||
// Of - E cannot be inferred, comes first
|
||||
func Of[E, A any](value A) Either[E, A]
|
||||
// Usage - only E needs to be specified
|
||||
result := either.Of[error](42)
|
||||
```
|
||||
|
||||
### Benefits
|
||||
|
||||
1. **Less verbose code**: Most operations don't require explicit type parameters
|
||||
2. **Better IDE support**: Type inference provides better autocomplete
|
||||
3. **Clearer intent**: Only specify types that can't be inferred
|
||||
|
||||
## Generic Type Aliases
|
||||
|
||||
fp-go v2 leverages Go 1.24's generic type aliases for cleaner type definitions:
|
||||
|
||||
```go
|
||||
// V2 - using generic type alias (requires Go 1.24+)
|
||||
type ReaderIOEither[R, E, A any] = RD.Reader[R, IOE.IOEither[E, A]]
|
||||
|
||||
// V1 - using type definition (Go 1.18+)
|
||||
type ReaderIOEither[R, E, A any] RD.Reader[R, IOE.IOEither[E, A]]
|
||||
```
|
||||
|
||||
### Benefits
|
||||
|
||||
1. **True aliases**: The type is interchangeable with its definition
|
||||
2. **No namespace imports needed**: Can use types directly without package prefixes
|
||||
3. **Simpler codebase**: Eliminates the need for `generic` subpackages
|
||||
4. **Better composability**: Types compose more naturally
|
||||
|
||||
### Migration Pattern
|
||||
|
||||
```go
|
||||
// Define project-wide aliases once
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
)
|
||||
|
||||
type Option[A any] = option.Option[A]
|
||||
type Result[A any] = result.Result[A]
|
||||
type IOResult[A any] = ioresult.IOResult[A]
|
||||
|
||||
// Use throughout your codebase
|
||||
package myapp
|
||||
|
||||
import "myproject/types"
|
||||
|
||||
func process(input string) types.Result[types.Option[int]] {
|
||||
// implementation
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
For more information, see:
|
||||
- [README.md](./README.md) - Overview and quick start
|
||||
- [API Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2) - Complete API reference
|
||||
- [Samples](./samples/) - Practical examples
|
||||
816
v2/IDIOMATIC_COMPARISON.md
Normal file
816
v2/IDIOMATIC_COMPARISON.md
Normal file
@@ -0,0 +1,816 @@
|
||||
# Idiomatic vs Standard Package Comparison
|
||||
|
||||
> **Latest Update:** 2025-11-18 - Updated with fresh benchmarks after `either` package optimizations
|
||||
|
||||
This document provides a comprehensive comparison between the `idiomatic` packages and the standard fp-go packages (`result` and `option`).
|
||||
|
||||
**See also:** [BENCHMARK_COMPARISON.md](./BENCHMARK_COMPARISON.md) for detailed performance analysis.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Design Differences](#design-differences)
|
||||
3. [Performance Comparison](#performance-comparison)
|
||||
4. [API Comparison](#api-comparison)
|
||||
5. [When to Use Each](#when-to-use-each)
|
||||
|
||||
## Overview
|
||||
|
||||
The fp-go library provides two approaches to functional programming patterns in Go:
|
||||
|
||||
- **Standard Packages** (`result`, `either`, `option`): Use struct wrappers for algebraic data types
|
||||
- **Idiomatic Packages** (`idiomatic/result`, `idiomatic/option`): Use native Go tuples for the same patterns
|
||||
|
||||
### Key Insight
|
||||
|
||||
After recent optimizations to the `either` package, both approaches now offer excellent performance:
|
||||
|
||||
- **Simple operations** (~1-5 ns/op): Both packages perform comparably
|
||||
- **Core transformations**: Idiomatic is **1.2-2.3x faster**
|
||||
- **Complex operations**: Idiomatic is **2-32x faster** with significantly fewer allocations
|
||||
- **Real-world pipelines**: Idiomatic shows **2-3.4x speedup**
|
||||
|
||||
The idiomatic packages provide:
|
||||
- Consistently better performance across most operations
|
||||
- Zero allocations for complex operations (ChainFirst: 72 B → 0 B)
|
||||
- More familiar Go idioms
|
||||
- Seamless integration with existing Go code
|
||||
|
||||
## Design Differences
|
||||
|
||||
### Data Representation
|
||||
|
||||
#### Standard Result Package
|
||||
|
||||
```go
|
||||
// Uses Either[error, A] which is a struct wrapper
|
||||
type Result[A any] = Either[error, A]
|
||||
type Either[E, A any] struct {
|
||||
r A
|
||||
l E
|
||||
isLeft bool
|
||||
}
|
||||
|
||||
// Creating values - ZERO heap allocations (struct returned by value)
|
||||
success := result.Right[error](42) // Returns Either struct by value (0 B/op)
|
||||
failure := result.Left[int](err) // Returns Either struct by value (0 B/op)
|
||||
|
||||
// Benchmarks confirm:
|
||||
// BenchmarkRight-16 871258489 1.384 ns/op 0 B/op 0 allocs/op
|
||||
// BenchmarkLeft-16 683089270 1.761 ns/op 0 B/op 0 allocs/op
|
||||
```
|
||||
|
||||
#### Idiomatic Result Package
|
||||
|
||||
```go
|
||||
// Uses native Go tuples (value, error)
|
||||
type Kleisli[A, B any] = func(A) (B, error)
|
||||
type Operator[A, B any] = func(A, error) (B, error)
|
||||
|
||||
// Creating values - ZERO allocations (tuples on stack)
|
||||
success := result.Right(42) // Returns (42, nil) - 0 B/op
|
||||
failure := result.Left[int](err) // Returns (0, err) - 0 B/op
|
||||
|
||||
// Benchmarks confirm:
|
||||
// BenchmarkRight-16 789879016 1.427 ns/op 0 B/op 0 allocs/op
|
||||
// BenchmarkLeft-16 895412131 1.349 ns/op 0 B/op 0 allocs/op
|
||||
```
|
||||
|
||||
### Type Signatures
|
||||
|
||||
#### Standard Result
|
||||
|
||||
```go
|
||||
// Functions take and return Result[T] structs
|
||||
func Map[A, B any](f func(A) B) func(Result[A]) Result[B]
|
||||
func Chain[A, B any](f Kleisli[A, B]) func(Result[A]) Result[B]
|
||||
func Fold[A, B any](onLeft func(error) B, onRight func(A) B) func(Result[A]) B
|
||||
|
||||
// Usage requires wrapping/unwrapping
|
||||
result := result.Right[error](42)
|
||||
mapped := result.Map(double)(result)
|
||||
value, err := result.UnwrapError(mapped)
|
||||
```
|
||||
|
||||
#### Idiomatic Result
|
||||
|
||||
```go
|
||||
// Functions work directly with tuples
|
||||
func Map[A, B any](f func(A) B) func(A, error) (B, error)
|
||||
func Chain[A, B any](f Kleisli[A, B]) func(A, error) (B, error)
|
||||
func Fold[A, B any](onLeft func(error) B, onRight func(A) B) func(A, error) B
|
||||
|
||||
// Usage works naturally with Go's error handling
|
||||
value, err := result.Right(42)
|
||||
value, err = result.Map(double)(value, err)
|
||||
// Can use directly: if err != nil { ... }
|
||||
```
|
||||
|
||||
### Memory Layout
|
||||
|
||||
#### Standard Result (struct-based)
|
||||
|
||||
```
|
||||
Either[error, int] struct (returned by value):
|
||||
┌─────────────────────┐
|
||||
│ r: int (8B) │ Stack allocation: 24 bytes
|
||||
│ l: error (8B) │ NO heap allocation when returned by value
|
||||
│ isLeft: bool (1B) │ Benchmarks show 0 B/op, 0 allocs/op
|
||||
│ padding (7B) │
|
||||
└─────────────────────┘
|
||||
|
||||
Key insight: Go returns small structs (<= ~64 bytes) by value on the stack.
|
||||
The Either struct (24 bytes) does NOT escape to heap in normal usage.
|
||||
```
|
||||
|
||||
#### Idiomatic Result (tuple-based)
|
||||
|
||||
```
|
||||
(int, error) tuple:
|
||||
┌─────────────────────┐
|
||||
│ int: 8 bytes │ Stack allocation: 16 bytes
|
||||
│ error: 8 bytes │ NO heap allocation
|
||||
└─────────────────────┘
|
||||
|
||||
Both approaches achieve zero heap allocations for constructor operations!
|
||||
```
|
||||
|
||||
### Why Both Have Zero Allocations
|
||||
|
||||
Both packages avoid heap allocations for simple operations:
|
||||
|
||||
**Standard Either/Result:**
|
||||
- `Either` struct is small (24 bytes)
|
||||
- Go returns by value on the stack
|
||||
- Inlining eliminates function call overhead
|
||||
- Result: `0 B/op, 0 allocs/op`
|
||||
|
||||
**Idiomatic Result:**
|
||||
- Tuples are native Go multi-value returns
|
||||
- Always on stack, never heap
|
||||
- Even simpler than structs
|
||||
- Result: `0 B/op, 0 allocs/op`
|
||||
|
||||
**When Either WOULD escape to heap:**
|
||||
```go
|
||||
// Taking address of local Either
|
||||
func bad1() *Either[error, int] {
|
||||
e := Right[error](42)
|
||||
return &e // ESCAPES: pointer to local
|
||||
}
|
||||
|
||||
// Storing in interface
|
||||
func bad2() interface{} {
|
||||
return Right[error](42) // ESCAPES: interface boxing
|
||||
}
|
||||
|
||||
// Closure capture with pointer receiver
|
||||
func bad3() func() Either[error, int] {
|
||||
e := Right[error](42)
|
||||
return func() Either[error, int] {
|
||||
return e // May escape depending on usage
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In normal functional composition (Map, Chain, Fold), neither package causes heap allocations for simple operations.
|
||||
|
||||
## Performance Comparison
|
||||
|
||||
> **Latest benchmarks:** 2025-11-18 after `either` package optimizations
|
||||
>
|
||||
> For detailed analysis, see [BENCHMARK_COMPARISON.md](./BENCHMARK_COMPARISON.md)
|
||||
|
||||
### Quick Summary (Either vs Idiomatic)
|
||||
|
||||
Both packages now show **excellent performance** after optimizations:
|
||||
|
||||
| Category | Either | Idiomatic | Winner | Speedup |
|
||||
|----------|--------|-----------|--------|---------|
|
||||
| **Constructors** | 1.4-1.8 ns/op | 1.2-1.4 ns/op | **TIE** | ~1.0-1.3x |
|
||||
| **Predicates** | 1.5 ns/op | 1.3-1.5 ns/op | **TIE** | ~1.0x |
|
||||
| **Map Operations** | 4.2-7.2 ns/op | 2.5-4.3 ns/op | **Idiomatic** | 1.2-2.1x |
|
||||
| **Chain Operations** | 4.4-5.4 ns/op | 2.3-2.5 ns/op | **Idiomatic** | 1.8-2.3x |
|
||||
| **ChainFirst** | **87.6 ns/op** (72 B) | **2.7 ns/op** (0 B) | **Idiomatic** | **32.4x** ✓✓✓ |
|
||||
| **BiMap** | 11.5-16.8 ns/op | 3.5-3.8 ns/op | **Idiomatic** | 3.3-4.4x |
|
||||
| **Alt/OrElse** | 4.0-5.7 ns/op | 2.4 ns/op | **Idiomatic** | 1.6-2.4x |
|
||||
| **GetOrElse** | 6.3-9.0 ns/op | 1.5-2.1 ns/op | **Idiomatic** | 3.1-6.1x |
|
||||
| **Pipelines** | 75-280 ns/op | 26-116 ns/op | **Idiomatic** | 2.4-3.4x |
|
||||
|
||||
### Constructor Operations
|
||||
|
||||
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Winner |
|
||||
|-----------|----------------|-------------------|---------|--------|
|
||||
| Left | 1.76 | **1.35** | 1.3x | Idiomatic ✓ |
|
||||
| Right | 1.38 | 1.43 | ~1.0x | Tie |
|
||||
| Of | 1.68 | **1.22** | 1.4x | Idiomatic ✓ |
|
||||
|
||||
**Analysis:** After optimizations, both packages have comparable constructor performance.
|
||||
|
||||
### Core Transformation Operations
|
||||
|
||||
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Winner |
|
||||
|------------------|----------------|-------------------|---------|--------|
|
||||
| Map (Right) | 5.13 | **4.34** | 1.2x | Idiomatic ✓ |
|
||||
| Map (Left) | 4.19 | **2.48** | 1.7x | Idiomatic ✓ |
|
||||
| MapLeft (Right) | 3.93 | **2.22** | 1.8x | Idiomatic ✓ |
|
||||
| MapLeft (Left) | 7.22 | **3.51** | 2.1x | Idiomatic ✓ |
|
||||
| Chain (Right) | 5.44 | **2.34** | 2.3x | Idiomatic ✓ |
|
||||
| Chain (Left) | 4.44 | **2.53** | 1.8x | Idiomatic ✓ |
|
||||
|
||||
### Complex Operations - The Big Difference
|
||||
|
||||
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|
||||
|-----------------------|----------------|-------------------|---------|---------------|-------------|
|
||||
| **ChainFirst (Right)** | **87.62** | **2.71** | **32.4x** ✓✓✓ | 72 B, 3 allocs | **0 B, 0 allocs** |
|
||||
| ChainFirst (Left) | 3.94 | 2.48 | 1.6x | 0 B | 0 B |
|
||||
| BiMap (Right) | 16.79 | **3.82** | 4.4x | 0 B | 0 B |
|
||||
| BiMap (Left) | 11.47 | **3.47** | 3.3x | 0 B | 0 B |
|
||||
|
||||
**Critical Insight:** ChainFirst shows the most dramatic difference - **32x faster** with **zero allocations** in idiomatic.
|
||||
|
||||
### Pipeline Benchmarks (Real-World Scenarios)
|
||||
|
||||
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|
||||
|-----------|----------------|-------------------|---------|---------------|-------------|
|
||||
| Pipeline Map (Right) | 112.7 | **46.5** | **2.4x** ✓ | 72 B, 3 allocs | 48 B, 2 allocs |
|
||||
| Pipeline Chain (Right) | 74.4 | **26.1** | **2.9x** ✓ | 48 B, 2 allocs | 24 B, 1 alloc |
|
||||
| Pipeline Complex (Right)| 279.8 | **116.3** | **2.4x** ✓ | 192 B, 8 allocs | 120 B, 5 allocs |
|
||||
|
||||
**Analysis:** In realistic composition scenarios, idiomatic is consistently 2-3x faster with fewer allocations.
|
||||
|
||||
### Extraction Operations
|
||||
|
||||
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Winner |
|
||||
|-----------|----------------|-------------------|---------|--------|
|
||||
| GetOrElse (Right) | 9.01 | **1.49** | **6.1x** ✓✓ | Idiomatic |
|
||||
| GetOrElse (Left) | 6.35 | **2.08** | **3.1x** ✓✓ | Idiomatic |
|
||||
| Alt (Right) | 5.72 | **2.40** | **2.4x** ✓ | Idiomatic |
|
||||
| Alt (Left) | 4.89 | **2.39** | **2.0x** ✓ | Idiomatic |
|
||||
| Fold (Right) | 4.03 | **2.75** | **1.5x** ✓ | Idiomatic |
|
||||
| Fold (Left) | 3.69 | **2.40** | **1.5x** ✓ | Idiomatic |
|
||||
|
||||
**Analysis:** Idiomatic shows significant advantages (1.5-6x) for value extraction operations.
|
||||
|
||||
### Key Findings After Optimizations
|
||||
|
||||
1. **Both packages are now fast** - Simple operations are in the 1-5 ns/op range for both
|
||||
2. **Idiomatic leads in most operations** - 1.2-2.3x faster for common transformations
|
||||
3. **ChainFirst is the standout** - 32x faster with zero allocations in idiomatic
|
||||
4. **Pipelines favor idiomatic** - 2-3.4x faster in realistic composition scenarios
|
||||
5. **Memory efficiency** - Idiomatic consistently uses fewer allocations
|
||||
|
||||
### Performance Summary
|
||||
|
||||
**Idiomatic Advantages:**
|
||||
- **Core operations**: 1.2-2.3x faster for Map, Chain, Fold
|
||||
- **Complex operations**: 3-32x faster with zero allocations
|
||||
- **Pipelines**: 2-3.4x faster with significantly fewer allocations
|
||||
- **Extraction**: 1.5-6x faster for GetOrElse, Alt, Fold
|
||||
- **Consistency**: Predictable, fast performance across all operations
|
||||
|
||||
**Either Advantages:**
|
||||
- **Comparable performance**: After optimizations, matches idiomatic for simple operations
|
||||
- **Feature richness**: More operations (Do-notation, Bind, Let, Flatten, Swap)
|
||||
- **Type flexibility**: Full Either[E, A] with custom error types
|
||||
- **Zero allocations**: Most simple operations have zero allocations
|
||||
|
||||
## API Comparison
|
||||
|
||||
### Creating Values
|
||||
|
||||
#### Standard Result
|
||||
|
||||
```go
|
||||
import "github.com/IBM/fp-go/v2/result"
|
||||
|
||||
// Create success/failure
|
||||
success := result.Right[error](42)
|
||||
failure := result.Left[int](errors.New("oops"))
|
||||
|
||||
// Type annotation required
|
||||
var r result.Result[int] = result.Right[error](42)
|
||||
```
|
||||
|
||||
#### Idiomatic Result
|
||||
|
||||
```go
|
||||
import "github.com/IBM/fp-go/v2/idiomatic/result"
|
||||
|
||||
// Create success/failure (more concise)
|
||||
success := result.Right(42) // (42, nil)
|
||||
failure := result.Left[int](errors.New("oops")) // (0, error)
|
||||
|
||||
// Native Go pattern
|
||||
value, err := result.Right(42)
|
||||
if err != nil {
|
||||
// handle error
|
||||
}
|
||||
```
|
||||
|
||||
### Transforming Values
|
||||
|
||||
#### Standard Result
|
||||
|
||||
```go
|
||||
// Map transforms the success value
|
||||
double := result.Map(N.Mul(2))
|
||||
result := double(result.Right[error](21)) // Right(42)
|
||||
|
||||
// Chain sequences operations
|
||||
validate := result.Chain(func(x int) result.Result[int] {
|
||||
if x > 0 {
|
||||
return result.Right[error](x * 2)
|
||||
}
|
||||
return result.Left[int](errors.New("negative"))
|
||||
})
|
||||
```
|
||||
|
||||
#### Idiomatic Result
|
||||
|
||||
```go
|
||||
// Map transforms the success value
|
||||
double := result.Map(N.Mul(2))
|
||||
value, err := double(21, nil) // (42, nil)
|
||||
|
||||
// Chain sequences operations
|
||||
validate := result.Chain(func(x int) (int, error) {
|
||||
if x > 0 {
|
||||
return x * 2, nil
|
||||
}
|
||||
return 0, errors.New("negative")
|
||||
})
|
||||
```
|
||||
|
||||
### Pattern Matching
|
||||
|
||||
#### Standard Result
|
||||
|
||||
```go
|
||||
// Fold extracts the value
|
||||
output := result.Fold(
|
||||
func(err error) string { return "Error: " + err.Error() },
|
||||
func(n int) string { return fmt.Sprintf("Value: %d", n) },
|
||||
)(myResult)
|
||||
|
||||
// GetOrElse with default
|
||||
value := result.GetOrElse(func(err error) int { return 0 })(myResult)
|
||||
```
|
||||
|
||||
#### Idiomatic Result
|
||||
|
||||
```go
|
||||
// Fold extracts the value (same API, different input)
|
||||
output := result.Fold(
|
||||
func(err error) string { return "Error: " + err.Error() },
|
||||
func(n int) string { return fmt.Sprintf("Value: %d", n) },
|
||||
)(value, err)
|
||||
|
||||
// GetOrElse with default
|
||||
value := result.GetOrElse(func(err error) int { return 0 })(value, err)
|
||||
|
||||
// Or use native Go pattern
|
||||
if err != nil {
|
||||
value = 0
|
||||
}
|
||||
```
|
||||
|
||||
### Integration with Existing Code
|
||||
|
||||
#### Standard Result
|
||||
|
||||
```go
|
||||
// Converting from (value, error) to Result
|
||||
func doSomething() (int, error) {
|
||||
return 42, nil
|
||||
}
|
||||
|
||||
result := result.TryCatchError(doSomething())
|
||||
|
||||
// Converting back to (value, error)
|
||||
value, err := result.UnwrapError(result)
|
||||
```
|
||||
|
||||
#### Idiomatic Result
|
||||
|
||||
```go
|
||||
// Direct compatibility with (value, error)
|
||||
func doSomething() (int, error) {
|
||||
return 42, nil
|
||||
}
|
||||
|
||||
// No conversion needed!
|
||||
value, err := doSomething()
|
||||
value, err = result.Map(double)(value, err)
|
||||
```
|
||||
|
||||
### Pipeline Composition
|
||||
|
||||
#### Standard Result
|
||||
|
||||
```go
|
||||
import F "github.com/IBM/fp-go/v2/function"
|
||||
|
||||
output := F.Pipe3(
|
||||
result.Right[error](10),
|
||||
result.Map(double),
|
||||
result.Chain(validate),
|
||||
result.Map(format),
|
||||
)
|
||||
|
||||
// Need to unwrap at the end
|
||||
value, err := result.UnwrapError(output)
|
||||
```
|
||||
|
||||
#### Idiomatic Result
|
||||
|
||||
```go
|
||||
import F "github.com/IBM/fp-go/v2/function"
|
||||
|
||||
value, err := F.Pipe3(
|
||||
result.Right(10),
|
||||
result.Map(double),
|
||||
result.Chain(validate),
|
||||
result.Map(format),
|
||||
)
|
||||
|
||||
// Already in (value, error) form
|
||||
if err != nil {
|
||||
// handle error
|
||||
}
|
||||
```
|
||||
|
||||
## Detailed Design Comparison
|
||||
|
||||
### Type System
|
||||
|
||||
#### Standard Result
|
||||
|
||||
**Strengths:**
|
||||
- Full algebraic data type semantics
|
||||
- Explicit Either[E, A] allows custom error types
|
||||
- Type-safe by construction
|
||||
- Clear separation of error and success channels
|
||||
|
||||
**Weaknesses:**
|
||||
- Requires wrapper structs (memory overhead)
|
||||
- Less familiar to Go developers
|
||||
- Needs conversion functions for Go's standard library
|
||||
- More verbose type annotations
|
||||
|
||||
#### Idiomatic Result
|
||||
|
||||
**Strengths:**
|
||||
- Native Go idioms (value, error) pattern
|
||||
- Zero wrapper overhead
|
||||
- Seamless stdlib integration
|
||||
- Familiar to all Go developers
|
||||
- Terser syntax
|
||||
|
||||
**Weaknesses:**
|
||||
- Error type fixed to `error`
|
||||
- Less explicit about Either semantics
|
||||
- Cannot use custom error types without conversion
|
||||
- Slightly less type-safe (can accidentally ignore bool/error)
|
||||
|
||||
### Monad Laws
|
||||
|
||||
Both packages satisfy the monad laws, but enforce them differently:
|
||||
|
||||
#### Standard Result
|
||||
|
||||
```go
|
||||
// Left identity: return a >>= f ≡ f a
|
||||
assert.Equal(
|
||||
result.Chain(f)(result.Of(a)),
|
||||
f(a),
|
||||
)
|
||||
|
||||
// Right identity: m >>= return ≡ m
|
||||
assert.Equal(
|
||||
result.Chain(result.Of[int])(m),
|
||||
m,
|
||||
)
|
||||
|
||||
// Associativity: (m >>= f) >>= g ≡ m >>= (\x -> f x >>= g)
|
||||
assert.Equal(
|
||||
result.Chain(g)(result.Chain(f)(m)),
|
||||
result.Chain(func(x int) result.Result[int] {
|
||||
return result.Chain(g)(f(x))
|
||||
})(m),
|
||||
)
|
||||
```
|
||||
|
||||
#### Idiomatic Result
|
||||
|
||||
```go
|
||||
// Same laws, different syntax
|
||||
// Left identity
|
||||
a, aerr := result.Of(val)
|
||||
b, berr := result.Chain(f)(a, aerr)
|
||||
c, cerr := f(val)
|
||||
assert.Equal((b, berr), (c, cerr))
|
||||
|
||||
// Right identity
|
||||
value, err := m()
|
||||
identity := result.Chain(result.Of[int])
|
||||
assert.Equal(identity(value, err), (value, err))
|
||||
|
||||
// Associativity (same structure, tuple-based)
|
||||
```
|
||||
|
||||
### Error Handling Philosophy
|
||||
|
||||
#### Standard Result
|
||||
|
||||
```go
|
||||
// Explicit error handling through types
|
||||
func processUser(id int) result.Result[User] {
|
||||
user := fetchUser(id) // Returns Result[User]
|
||||
|
||||
return F.Pipe2(
|
||||
user,
|
||||
result.Chain(validateUser),
|
||||
result.Chain(enrichUser),
|
||||
)
|
||||
}
|
||||
|
||||
// Must explicitly unwrap
|
||||
user, err := result.UnwrapError(processUser(42))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
```
|
||||
|
||||
#### Idiomatic Result
|
||||
|
||||
```go
|
||||
// Natural Go error handling
|
||||
func processUser(id int) (User, error) {
|
||||
user, err := fetchUser(id) // Returns (User, error)
|
||||
|
||||
return F.Pipe2(
|
||||
(user, err),
|
||||
result.Chain(validateUser),
|
||||
result.Chain(enrichUser),
|
||||
)
|
||||
}
|
||||
|
||||
// Already in Go form
|
||||
user, err := processUser(42)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
```
|
||||
|
||||
### Composition Patterns
|
||||
|
||||
#### Standard Result
|
||||
|
||||
```go
|
||||
// Applicative composition
|
||||
import A "github.com/IBM/fp-go/v2/apply"
|
||||
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
DB string
|
||||
}
|
||||
|
||||
config := A.SequenceT3(
|
||||
result.FromPredicate(validHost, hostError)(host),
|
||||
result.FromPredicate(validPort, portError)(port),
|
||||
result.FromPredicate(validDB, dbError)(db),
|
||||
)(func(h string, p int, d string) Config {
|
||||
return Config{h, p, d}
|
||||
})
|
||||
```
|
||||
|
||||
#### Idiomatic Result
|
||||
|
||||
```go
|
||||
// Direct tuple composition
|
||||
config, err := func() (Config, error) {
|
||||
host, err := result.FromPredicate(validHost, hostError)(host)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
port, err := result.FromPredicate(validPort, portError)(port)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
db, err := result.FromPredicate(validDB, dbError)(db)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
return Config{host, port, db}, nil
|
||||
}()
|
||||
```
|
||||
|
||||
## When to Use Each
|
||||
|
||||
### Use Idiomatic Result When (Recommended for Most Cases):
|
||||
|
||||
1. **Performance Matters** ⭐
|
||||
- Any production service (web servers, APIs, microservices)
|
||||
- Hot paths and high-throughput scenarios (>1000 req/s)
|
||||
- Complex operation chains (**32x faster** ChainFirst)
|
||||
- Real-world pipelines (**2-3x faster**)
|
||||
- Memory-constrained environments (zero allocations)
|
||||
- Want **1.2-6x speedup** across most operations
|
||||
|
||||
2. **Go Integration** ⭐⭐
|
||||
- Working with existing Go codebases
|
||||
- Interfacing with standard library (native (value, error))
|
||||
- Team familiar with Go, new to FP
|
||||
- Want zero-cost functional abstractions
|
||||
- Seamless error handling patterns
|
||||
|
||||
3. **Pragmatic Functional Programming**
|
||||
- Value performance AND functional patterns
|
||||
- Prefer Go idioms over FP terminology
|
||||
- Simpler function signatures
|
||||
- Lower cognitive overhead
|
||||
- Production-ready patterns
|
||||
|
||||
4. **Real-World Applications**
|
||||
- Web servers, REST APIs, gRPC services
|
||||
- CLI tools and command-line applications
|
||||
- Data processing pipelines
|
||||
- Any latency-sensitive application
|
||||
- Systems with tight performance budgets
|
||||
|
||||
**Performance Gains:** Use idiomatic for 1.2-32x speedup depending on operation, with consistently lower allocations.
|
||||
|
||||
### Use Standard Either/Result When:
|
||||
|
||||
1. **Type Safety & Flexibility**
|
||||
- Need explicit Either[E, A] with **custom error types**
|
||||
- Building domain-specific error hierarchies
|
||||
- Want to distinguish different error categories at type level
|
||||
- Type system enforcement is critical
|
||||
|
||||
2. **Advanced FP Features**
|
||||
- Using Do-notation for complex monadic compositions
|
||||
- Need operations like Flatten, Swap, Bind, Let
|
||||
- Leveraging advanced type classes (Semigroup, Monoid)
|
||||
- Want the complete FP toolkit
|
||||
|
||||
3. **FP Expertise & Education**
|
||||
- Porting code from other FP languages (Scala, Haskell)
|
||||
- Teaching functional programming concepts
|
||||
- Team has strong FP background
|
||||
- Explicit algebraic data types preferred
|
||||
- Code review benefits from FP terminology
|
||||
|
||||
4. **Performance is Acceptable**
|
||||
- After optimizations, Either is **quite fast** (1-5 ns/op for simple operations)
|
||||
- Difference matters mainly at high scale (millions of operations)
|
||||
- Code clarity > micro-optimizations
|
||||
- Simple operations dominate your workload
|
||||
|
||||
**Note:** Either package is now performant enough for most use cases. Choose it for features, not performance concerns.
|
||||
|
||||
### Hybrid Approach
|
||||
|
||||
You can use both packages together:
|
||||
|
||||
```go
|
||||
import (
|
||||
stdResult "github.com/IBM/fp-go/v2/result"
|
||||
"github.com/IBM/fp-go/v2/idiomatic/result"
|
||||
)
|
||||
|
||||
// Use standard for complex types
|
||||
type ValidationError struct {
|
||||
Field string
|
||||
Error string
|
||||
}
|
||||
|
||||
func validateInput(input string) stdResult.Either[ValidationError, Input] {
|
||||
// ... validation logic
|
||||
}
|
||||
|
||||
// Convert to idiomatic for performance
|
||||
func processInput(input string) (Output, error) {
|
||||
validated := validateInput(input)
|
||||
value, err := stdResult.UnwrapError(
|
||||
stdResult.MapLeft(toError)(validated),
|
||||
)
|
||||
|
||||
// Use idiomatic for hot path
|
||||
return result.Chain(heavyProcessing)(value, err)
|
||||
}
|
||||
```
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### From Standard to Idiomatic
|
||||
|
||||
```go
|
||||
// Before (standard)
|
||||
import "github.com/IBM/fp-go/v2/result"
|
||||
|
||||
func process(x int) result.Result[int] {
|
||||
return F.Pipe2(
|
||||
result.Right[error](x),
|
||||
result.Map(double),
|
||||
result.Chain(validate),
|
||||
)
|
||||
}
|
||||
|
||||
// After (idiomatic)
|
||||
import "github.com/IBM/fp-go/v2/idiomatic/result"
|
||||
|
||||
func process(x int) (int, error) {
|
||||
return F.Pipe2(
|
||||
result.Right(x),
|
||||
result.Map(double),
|
||||
result.Chain(validate),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Key Changes
|
||||
|
||||
1. **Type signatures**: `Result[T]` → `(T, error)`
|
||||
2. **Kleisli**: `func(A) Result[B]` → `func(A) (B, error)`
|
||||
3. **Operator**: `func(Result[A]) Result[B]` → `func(A, error) (B, error)`
|
||||
4. **Return values**: Function calls return tuples, not wrapped values
|
||||
5. **Pattern matching**: Same Fold/GetOrElse API, different inputs
|
||||
|
||||
## Conclusion
|
||||
|
||||
### Performance Summary (After Either Optimizations)
|
||||
|
||||
The latest benchmark results show a clear pattern:
|
||||
|
||||
**Both packages are now fast**, but idiomatic consistently leads:
|
||||
|
||||
- **Constructors & Predicates**: Both ~1-2 ns/op (essentially tied)
|
||||
- **Core transformations**: Idiomatic **1.2-2.3x faster** (Map, Chain, Fold)
|
||||
- **Complex operations**: Idiomatic **3-32x faster** (BiMap, ChainFirst)
|
||||
- **Pipelines**: Idiomatic **2-3.4x faster** with fewer allocations
|
||||
- **Extraction**: Idiomatic **1.5-6x faster** (GetOrElse, Alt)
|
||||
|
||||
**Key Insight:** The idiomatic package delivers **consistently better performance** across the board while maintaining zero-cost abstractions. The Either package is now fast enough for most use cases, but idiomatic is the performance winner.
|
||||
|
||||
### Updated Recommendation Matrix
|
||||
|
||||
| Scenario | Recommendation | Reason |
|
||||
|----------|---------------|--------|
|
||||
| **New Go project** | **Idiomatic** ⭐ | Natural Go patterns, 1.2-6x faster, better integration |
|
||||
| **Production services** | **Idiomatic** ⭐⭐ | 2-3x faster pipelines, zero allocations, proven performance |
|
||||
| **Performance critical** | **Idiomatic** ⭐⭐⭐ | 32x faster complex ops, minimal allocations |
|
||||
| **Microservices/APIs** | **Idiomatic** ⭐⭐ | High throughput, familiar patterns, better performance |
|
||||
| **CLI Tools** | **Idiomatic** ⭐ | Low overhead, Go idioms, fast |
|
||||
| Custom error types | Standard/Either | Need Either[E, A] with domain types |
|
||||
| Learning FP | Standard/Either | Clearer ADT semantics, educational |
|
||||
| FP-heavy codebase | Standard/Either | Consistency, Do-notation, full FP toolkit |
|
||||
| Library/Framework | Either way | Both are good; choose based on API style |
|
||||
|
||||
### Real-World Impact
|
||||
|
||||
For a service handling 10,000 requests/second with typical pipeline operations:
|
||||
|
||||
```
|
||||
Either package: 280 ns/op × 10M req/day = 2,800 seconds = 46.7 minutes
|
||||
Idiomatic package: 116 ns/op × 10M req/day = 1,160 seconds = 19.3 minutes
|
||||
Time saved: 27.4 minutes of CPU time per day
|
||||
```
|
||||
|
||||
At scale, this translates to:
|
||||
- Lower latency (2-3x faster response times for FP operations)
|
||||
- Reduced CPU usage (fewer cores needed)
|
||||
- Lower memory pressure (significantly fewer allocations)
|
||||
- Better resource utilization
|
||||
|
||||
### Final Recommendation
|
||||
|
||||
**For most Go projects:** Use **idiomatic packages**
|
||||
- 1.2-32x faster across operations
|
||||
- Native Go idioms
|
||||
- Zero-cost abstractions
|
||||
- Production-proven performance
|
||||
- Easier integration
|
||||
|
||||
**For specialized needs:** Use **standard Either/Result**
|
||||
- Need custom error types Either[E, A]
|
||||
- Want Do-notation and advanced FP features
|
||||
- Porting from FP languages
|
||||
- Educational/learning context
|
||||
- FP-heavy existing codebase
|
||||
|
||||
### Bottom Line
|
||||
|
||||
After optimizations, both packages are excellent:
|
||||
|
||||
- **Either/Result**: Fast enough for most use cases, feature-rich, type-safe
|
||||
- **Idiomatic**: **Faster in practice** (1.2-32x), native Go, zero-cost FP
|
||||
|
||||
The idiomatic packages now represent the **best of both worlds**: full functional programming capabilities with Go's native performance and idioms. Unless you specifically need Either[E, A]'s custom error types or advanced FP features, **idiomatic is the recommended choice** for production Go services.
|
||||
|
||||
Both maintain the core benefits of functional programming—choose based on whether you prioritize performance & Go integration (idiomatic) or type flexibility & FP features (either).
|
||||
174
v2/IDIOMATIC_READERIORESULT_TODO.md
Normal file
174
v2/IDIOMATIC_READERIORESULT_TODO.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# Idiomatic ReadIOResult Functions - Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the idiomatic functions that should be added to the `readerioresult` package to support Go's native `(value, error)` pattern, similar to what was implemented for `readerresult`.
|
||||
|
||||
## Key Concepts
|
||||
|
||||
The idiomatic package `github.com/IBM/fp-go/v2/idiomatic/readerioresult` defines:
|
||||
- `ReaderIOResult[R, A]` as `func(R) func() (A, error)` (idiomatic style)
|
||||
- This contrasts with `readerioresult.ReaderIOResult[R, A]` which is `Reader[R, IOResult[A]]` (functional style)
|
||||
|
||||
## Functions to Add
|
||||
|
||||
### In `readerioresult/reader.go`
|
||||
|
||||
Add helper functions at the top:
|
||||
```go
|
||||
func fromReaderIOResultKleisliI[R, A, B any](f RIORI.Kleisli[R, A, B]) Kleisli[R, A, B] {
|
||||
return function.Flow2(f, FromReaderIOResultI[R, B])
|
||||
}
|
||||
|
||||
func fromIOResultKleisliI[A, B any](f IORI.Kleisli[A, B]) ioresult.Kleisli[A, B] {
|
||||
return ioresult.Eitherize1(f)
|
||||
}
|
||||
```
|
||||
|
||||
### Core Conversion Functions
|
||||
|
||||
1. **FromResultI** - Lift `(value, error)` to ReaderIOResult
|
||||
```go
|
||||
func FromResultI[R, A any](a A, err error) ReaderIOResult[R, A]
|
||||
```
|
||||
|
||||
2. **FromIOResultI** - Lift idiomatic IOResult to functional
|
||||
```go
|
||||
func FromIOResultI[R, A any](ioe func() (A, error)) ReaderIOResult[R, A]
|
||||
```
|
||||
|
||||
3. **FromReaderIOResultI** - Convert idiomatic ReaderIOResult to functional
|
||||
```go
|
||||
func FromReaderIOResultI[R, A any](rr RIORI.ReaderIOResult[R, A]) ReaderIOResult[R, A]
|
||||
```
|
||||
|
||||
### Chain Functions
|
||||
|
||||
4. **MonadChainI** / **ChainI** - Chain with idiomatic Kleisli
|
||||
```go
|
||||
func MonadChainI[R, A, B any](ma ReaderIOResult[R, A], f RIORI.Kleisli[R, A, B]) ReaderIOResult[R, B]
|
||||
func ChainI[R, A, B any](f RIORI.Kleisli[R, A, B]) Operator[R, A, B]
|
||||
```
|
||||
|
||||
5. **MonadChainEitherIK** / **ChainEitherIK** - Chain with idiomatic Result functions
|
||||
```go
|
||||
func MonadChainEitherIK[R, A, B any](ma ReaderIOResult[R, A], f func(A) (B, error)) ReaderIOResult[R, B]
|
||||
func ChainEitherIK[R, A, B any](f func(A) (B, error)) Operator[R, A, B]
|
||||
```
|
||||
|
||||
6. **MonadChainIOResultIK** / **ChainIOResultIK** - Chain with idiomatic IOResult
|
||||
```go
|
||||
func MonadChainIOResultIK[R, A, B any](ma ReaderIOResult[R, A], f func(A) func() (B, error)) ReaderIOResult[R, B]
|
||||
func ChainIOResultIK[R, A, B any](f func(A) func() (B, error)) Operator[R, A, B]
|
||||
```
|
||||
|
||||
### Applicative Functions
|
||||
|
||||
7. **MonadApI** / **ApI** - Apply with idiomatic value
|
||||
```go
|
||||
func MonadApI[B, R, A any](fab ReaderIOResult[R, func(A) B], fa RIORI.ReaderIOResult[R, A]) ReaderIOResult[R, B]
|
||||
func ApI[B, R, A any](fa RIORI.ReaderIOResult[R, A]) Operator[R, func(A) B, B]
|
||||
```
|
||||
|
||||
### Error Handling Functions
|
||||
|
||||
8. **OrElseI** - Fallback with idiomatic computation
|
||||
```go
|
||||
func OrElseI[R, A any](onLeft RIORI.Kleisli[R, error, A]) Operator[R, A, A]
|
||||
```
|
||||
|
||||
9. **MonadAltI** / **AltI** - Alternative with idiomatic computation
|
||||
```go
|
||||
func MonadAltI[R, A any](first ReaderIOResult[R, A], second Lazy[RIORI.ReaderIOResult[R, A]]) ReaderIOResult[R, A]
|
||||
func AltI[R, A any](second Lazy[RIORI.ReaderIOResult[R, A]]) Operator[R, A, A]
|
||||
```
|
||||
|
||||
### Flatten Functions
|
||||
|
||||
10. **FlattenI** - Flatten nested idiomatic ReaderIOResult
|
||||
```go
|
||||
func FlattenI[R, A any](mma ReaderIOResult[R, RIORI.ReaderIOResult[R, A]]) ReaderIOResult[R, A]
|
||||
```
|
||||
|
||||
### In `readerioresult/bind.go`
|
||||
|
||||
11. **BindI** - Bind with idiomatic Kleisli
|
||||
```go
|
||||
func BindI[R, S1, S2, T any](setter func(T) func(S1) S2, f RIORI.Kleisli[R, S1, T]) Operator[R, S1, S2]
|
||||
```
|
||||
|
||||
12. **ApIS** - Apply idiomatic value to state
|
||||
```go
|
||||
func ApIS[R, S1, S2, T any](setter func(T) func(S1) S2, fa RIORI.ReaderIOResult[R, T]) Operator[R, S1, S2]
|
||||
```
|
||||
|
||||
13. **ApISL** - Apply idiomatic value using lens
|
||||
```go
|
||||
func ApISL[R, S, T any](lens L.Lens[S, T], fa RIORI.ReaderIOResult[R, T]) Operator[R, S, S]
|
||||
```
|
||||
|
||||
14. **BindIL** - Bind idiomatic with lens
|
||||
```go
|
||||
func BindIL[R, S, T any](lens L.Lens[S, T], f RIORI.Kleisli[R, T, T]) Operator[R, S, S]
|
||||
```
|
||||
|
||||
15. **BindEitherIK** / **BindResultIK** - Bind idiomatic Result
|
||||
```go
|
||||
func BindEitherIK[R, S1, S2, T any](setter func(T) func(S1) S2, f func(S1) (T, error)) Operator[R, S1, S2]
|
||||
func BindResultIK[R, S1, S2, T any](setter func(T) func(S1) S2, f func(S1) (T, error)) Operator[R, S1, S2]
|
||||
```
|
||||
|
||||
16. **BindIOResultIK** - Bind idiomatic IOResult
|
||||
```go
|
||||
func BindIOResultIK[R, S1, S2, T any](setter func(T) func(S1) S2, f func(S1) func() (T, error)) Operator[R, S1, S2]
|
||||
```
|
||||
|
||||
17. **BindToEitherI** / **BindToResultI** - Initialize from idiomatic pair
|
||||
```go
|
||||
func BindToEitherI[R, S1, T any](setter func(T) S1) func(T, error) ReaderIOResult[R, S1]
|
||||
func BindToResultI[R, S1, T any](setter func(T) S1) func(T, error) ReaderIOResult[R, S1]
|
||||
```
|
||||
|
||||
18. **BindToIOResultI** - Initialize from idiomatic IOResult
|
||||
```go
|
||||
func BindToIOResultI[R, S1, T any](setter func(T) S1) func(func() (T, error)) ReaderIOResult[R, S1]
|
||||
```
|
||||
|
||||
19. **ApEitherIS** / **ApResultIS** - Apply idiomatic pair to state
|
||||
```go
|
||||
func ApEitherIS[R, S1, S2, T any](setter func(T) func(S1) S2) func(T, error) Operator[R, S1, S2]
|
||||
func ApResultIS[R, S1, S2, T any](setter func(T) func(S1) S2) func(T, error) Operator[R, S1, S2]
|
||||
```
|
||||
|
||||
20. **ApIOResultIS** - Apply idiomatic IOResult to state
|
||||
```go
|
||||
func ApIOResultIS[R, S1, S2, T any](setter func(T) func(S1) S2, fa func() (T, error)) Operator[R, S1, S2]
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Create `readerioresult/idiomatic_test.go` with:
|
||||
- Tests for each idiomatic function
|
||||
- Success and error cases
|
||||
- Integration tests showing real-world usage patterns
|
||||
- Parallel execution tests where applicable
|
||||
- Complex scenarios combining multiple idiomatic functions
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
1. **High Priority** - Core conversion and chain functions (1-6)
|
||||
2. **Medium Priority** - Bind functions for do-notation (11-16)
|
||||
3. **Low Priority** - Advanced applicative and error handling (7-10, 17-20)
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Seamless Integration** - Mix Go idiomatic code with functional pipelines
|
||||
2. **Gradual Adoption** - Convert code incrementally from idiomatic to functional
|
||||
3. **Interoperability** - Work with existing Go libraries that return `(value, error)`
|
||||
4. **Consistency** - Mirrors the successful pattern from `readerresult`
|
||||
|
||||
## References
|
||||
|
||||
- See `readerresult` package for similar implementations
|
||||
- See `idiomatic/readerresult` for the idiomatic types
|
||||
- See `idiomatic/ioresult` for IO-level idiomatic patterns
|
||||
490
v2/README.md
490
v2/README.md
@@ -1,43 +1,497 @@
|
||||
# Functional programming library for golang V2
|
||||
# fp-go V2: Enhanced Functional Programming for Go 1.24+
|
||||
|
||||
Go 1.24 introduces [generic type aliases](https://github.com/golang/go/issues/46477) which are leveraged by V2.
|
||||
[](https://pkg.go.dev/github.com/IBM/fp-go/v2)
|
||||
[](https://coveralls.io/github/IBM/fp-go?branch=main)
|
||||
[](https://goreportcard.com/report/github.com/IBM/fp-go/v2)
|
||||
|
||||
## ⚠️ Breaking Changes
|
||||
**fp-go** is a comprehensive functional programming library for Go, bringing type-safe functional patterns inspired by [fp-ts](https://gcanti.github.io/fp-ts/) to the Go ecosystem. Version 2 leverages [generic type aliases](https://github.com/golang/go/issues/46477) introduced in Go 1.24, providing a more ergonomic and streamlined API.
|
||||
|
||||
- use of [generic type aliases](https://github.com/golang/go/issues/46477) which requires [go1.24](https://tip.golang.org/doc/go1.24)
|
||||
- order of generic type arguments adjusted such that types that _cannot_ be inferred by the method argument come first, e.g. in the `Ap` methods
|
||||
- monadic operations for `Pair` operate on the second argument, to be compatible with the [Haskell](https://hackage.haskell.org/package/TypeCompose-0.9.14/docs/Data-Pair.html) definition
|
||||
## 📚 Table of Contents
|
||||
|
||||
## Simplifications
|
||||
- [Overview](#-overview)
|
||||
- [Features](#-features)
|
||||
- [Requirements](#-requirements)
|
||||
- [Installation](#-installation)
|
||||
- [Quick Start](#-quick-start)
|
||||
- [Breaking Changes](#️-breaking-changes)
|
||||
- [Key Improvements](#-key-improvements)
|
||||
- [Migration Guide](#-migration-guide)
|
||||
- [What's New](#-whats-new)
|
||||
- [Documentation](#-documentation)
|
||||
- [Contributing](#-contributing)
|
||||
- [License](#-license)
|
||||
|
||||
- use type aliases to get rid of namespace imports for type declarations, e.g. instead of
|
||||
## 🎯 Overview
|
||||
|
||||
fp-go brings the power of functional programming to Go with:
|
||||
|
||||
- **Type-safe abstractions** - Monads, Functors, Applicatives, and more
|
||||
- **Composable operations** - Build complex logic from simple, reusable functions
|
||||
- **Error handling** - Elegant error management with `Either`, `Result`, and `IOEither`
|
||||
- **Lazy evaluation** - Control when and how computations execute
|
||||
- **Optics** - Powerful lens, prism, and traversal operations for immutable data manipulation
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 🔒 **Type Safety** - Leverage Go's generics for compile-time guarantees
|
||||
- 🧩 **Composability** - Chain operations naturally with functional composition
|
||||
- 📦 **Rich Type System** - `Option`, `Either`, `Result`, `IO`, `Reader`, and more
|
||||
- 🎯 **Practical** - Designed for real-world Go applications
|
||||
- 🚀 **Performance** - Zero-cost abstractions where possible
|
||||
- 📖 **Well-documented** - Comprehensive API documentation and examples
|
||||
- 🧪 **Battle-tested** - Extensive test coverage
|
||||
|
||||
## 🔧 Requirements
|
||||
|
||||
- **Go 1.24 or later** (for generic type alias support)
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
```bash
|
||||
go get github.com/IBM/fp-go/v2
|
||||
```
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Working with Option
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
ET "github.com/IBM/fp-go/v2/either"
|
||||
"fmt"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
func doSth() ET.Either[error, string] {
|
||||
...
|
||||
func main() {
|
||||
// Create an Option
|
||||
some := option.Some(42)
|
||||
none := option.None[int]()
|
||||
|
||||
// Map over values
|
||||
doubled := option.Map(N.Mul(2))(some)
|
||||
fmt.Println(option.GetOrElse(0)(doubled)) // Output: 84
|
||||
|
||||
// Chain operations
|
||||
result := option.Chain(func(x int) option.Option[string] {
|
||||
if x > 0 {
|
||||
return option.Some(fmt.Sprintf("Positive: %d", x))
|
||||
}
|
||||
return option.None[string]()
|
||||
})(some)
|
||||
|
||||
fmt.Println(option.GetOrElse("No value")(result)) // Output: Positive: 42
|
||||
}
|
||||
```
|
||||
|
||||
you can declare your type once
|
||||
### Error Handling with Result
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
func divide(a, b int) result.Result[int] {
|
||||
if b == 0 {
|
||||
return result.Error[int](errors.New("division by zero"))
|
||||
}
|
||||
return result.Ok(a / b)
|
||||
}
|
||||
|
||||
func main() {
|
||||
res := divide(10, 2)
|
||||
|
||||
// Pattern match on the result
|
||||
result.Fold(
|
||||
func(err error) { fmt.Println("Error:", err) },
|
||||
func(val int) { fmt.Println("Result:", val) },
|
||||
)(res)
|
||||
// Output: Result: 5
|
||||
|
||||
// Or use GetOrElse for a default value
|
||||
value := result.GetOrElse(0)(divide(10, 0))
|
||||
fmt.Println("Value:", value) // Output: Value: 0
|
||||
}
|
||||
```
|
||||
|
||||
### Composing IO Operations
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Define pure IO operations
|
||||
readInput := io.MakeIO(func() string {
|
||||
return "Hello, fp-go!"
|
||||
})
|
||||
|
||||
// Transform the result
|
||||
uppercase := io.Map(func(s string) string {
|
||||
return fmt.Sprintf(">>> %s <<<", s)
|
||||
})(readInput)
|
||||
|
||||
// Execute the IO operation
|
||||
result := uppercase()
|
||||
fmt.Println(result) // Output: >>> Hello, fp-go! <<<
|
||||
}
|
||||
```
|
||||
|
||||
### From V1 to V2
|
||||
|
||||
#### 1. Generic Type Aliases
|
||||
|
||||
V2 uses [generic type aliases](https://github.com/golang/go/issues/46477) which require Go 1.24+. This is the most significant change and enables cleaner type definitions.
|
||||
|
||||
**V1:**
|
||||
```go
|
||||
type ReaderIOEither[R, E, A any] RD.Reader[R, IOE.IOEither[E, A]]
|
||||
```
|
||||
|
||||
**V2:**
|
||||
```go
|
||||
type ReaderIOEither[R, E, A any] = RD.Reader[R, IOE.IOEither[E, A]]
|
||||
```
|
||||
|
||||
#### 2. Generic Type Parameter Ordering
|
||||
|
||||
Type parameters that **cannot** be inferred from function arguments now come first, improving type inference.
|
||||
|
||||
**V1:**
|
||||
```go
|
||||
// Ap in V1 - less intuitive ordering
|
||||
func Ap[R, E, A, B any](fa ReaderIOEither[R, E, A]) func(ReaderIOEither[R, E, func(A) B]) ReaderIOEither[R, E, B]
|
||||
```
|
||||
|
||||
**V2:**
|
||||
```go
|
||||
// Ap in V2 - B comes first as it cannot be inferred
|
||||
func Ap[B, R, E, A any](fa ReaderIOEither[R, E, A]) func(ReaderIOEither[R, E, func(A) B]) ReaderIOEither[R, E, B]
|
||||
```
|
||||
|
||||
This change allows the Go compiler to infer more types automatically, reducing the need for explicit type parameters.
|
||||
|
||||
#### 3. Pair Monad Semantics
|
||||
|
||||
Monadic operations for `Pair` now operate on the **second argument** to align with the [Haskell definition](https://hackage.haskell.org/package/TypeCompose-0.9.14/docs/Data-Pair.html).
|
||||
|
||||
**V1:**
|
||||
```go
|
||||
// Operations on first element
|
||||
pair := MakePair(1, "hello")
|
||||
result := Map(N.Mul(2))(pair) // Pair(2, "hello")
|
||||
```
|
||||
|
||||
**V2:**
|
||||
```go
|
||||
// Operations on second element (Haskell-compatible)
|
||||
pair := MakePair(1, "hello")
|
||||
result := Map(func(s string) string { return s + "!" })(pair) // Pair(1, "hello!")
|
||||
```
|
||||
|
||||
#### 4. Endomorphism Compose Semantics
|
||||
|
||||
The `Compose` function for endomorphisms now follows **mathematical function composition** (right-to-left execution), aligning with standard functional programming conventions.
|
||||
|
||||
**V1:**
|
||||
```go
|
||||
// Compose executed left-to-right
|
||||
double := N.Mul(2)
|
||||
increment := N.Add(1)
|
||||
composed := Compose(double, increment)
|
||||
result := composed(5) // (5 * 2) + 1 = 11
|
||||
```
|
||||
|
||||
**V2:**
|
||||
```go
|
||||
// Compose executes RIGHT-TO-LEFT (mathematical composition)
|
||||
double := N.Mul(2)
|
||||
increment := N.Add(1)
|
||||
composed := Compose(double, increment)
|
||||
result := composed(5) // (5 + 1) * 2 = 12
|
||||
|
||||
// Use MonadChain for LEFT-TO-RIGHT execution
|
||||
chained := MonadChain(double, increment)
|
||||
result2 := chained(5) // (5 * 2) + 1 = 11
|
||||
```
|
||||
|
||||
**Key Difference:**
|
||||
- `Compose(f, g)` now means `f ∘ g`, which applies `g` first, then `f` (right-to-left)
|
||||
- `MonadChain(f, g)` applies `f` first, then `g` (left-to-right)
|
||||
|
||||
## ✨ Key Improvements
|
||||
|
||||
### 1. Simplified Type Declarations
|
||||
|
||||
Generic type aliases eliminate the need for namespace imports in type declarations.
|
||||
|
||||
**V1 Approach:**
|
||||
```go
|
||||
import (
|
||||
ET "github.com/IBM/fp-go/either"
|
||||
OPT "github.com/IBM/fp-go/option"
|
||||
)
|
||||
|
||||
func processData(input string) ET.Either[error, OPT.Option[int]] {
|
||||
// implementation
|
||||
}
|
||||
```
|
||||
|
||||
**V2 Approach:**
|
||||
```go
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// Define type aliases once
|
||||
type Result[A any] = result.Result[A]
|
||||
type Option[A any] = option.Option[A]
|
||||
|
||||
// Use them throughout your codebase
|
||||
func processData(input string) Result[Option[int]] {
|
||||
// implementation
|
||||
}
|
||||
```
|
||||
|
||||
### 2. No More `generic` Subpackages
|
||||
|
||||
The library implementation no longer requires separate `generic` subpackages, making the codebase simpler and easier to understand.
|
||||
|
||||
**V1 Structure:**
|
||||
```
|
||||
either/
|
||||
either.go
|
||||
generic/
|
||||
either.go // Generic implementation
|
||||
```
|
||||
|
||||
**V2 Structure:**
|
||||
```
|
||||
either/
|
||||
either.go // Single, clean implementation
|
||||
```
|
||||
|
||||
### 3. Better Type Inference
|
||||
|
||||
The reordered type parameters allow the Go compiler to infer more types automatically:
|
||||
|
||||
**V1:**
|
||||
```go
|
||||
// Often need explicit type parameters
|
||||
result := Map[Context, error, int, string](transform)(value)
|
||||
```
|
||||
|
||||
**V2:**
|
||||
```go
|
||||
// Compiler can infer more types
|
||||
result := Map(transform)(value) // Cleaner!
|
||||
```
|
||||
|
||||
## 🚀 Migration Guide
|
||||
|
||||
### Step 1: Update Go Version
|
||||
|
||||
Ensure you're using Go 1.24 or later:
|
||||
|
||||
```bash
|
||||
go version # Should show go1.24 or higher
|
||||
```
|
||||
|
||||
### Step 2: Update Import Paths
|
||||
|
||||
Change all import paths from `github.com/IBM/fp-go` to `github.com/IBM/fp-go/v2`:
|
||||
|
||||
**Before:**
|
||||
```go
|
||||
import (
|
||||
"github.com/IBM/fp-go/either"
|
||||
"github.com/IBM/fp-go/option"
|
||||
)
|
||||
```
|
||||
|
||||
**After:**
|
||||
```go
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
```
|
||||
|
||||
### Step 3: Remove `generic` Subpackage Imports
|
||||
|
||||
If you were using generic subpackages, remove them:
|
||||
|
||||
**Before:**
|
||||
```go
|
||||
import (
|
||||
E "github.com/IBM/fp-go/either/generic"
|
||||
)
|
||||
```
|
||||
|
||||
**After:**
|
||||
```go
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
)
|
||||
|
||||
type Either[A any] = either.Either[error, A]
|
||||
```
|
||||
|
||||
and then use it across your codebase
|
||||
### Step 4: Update Type Parameter Order
|
||||
|
||||
Review functions like `Ap` where type parameter order has changed. The compiler will help identify these:
|
||||
|
||||
**Before:**
|
||||
```go
|
||||
result := Ap[Context, error, int, string](value)(funcInContext)
|
||||
```
|
||||
|
||||
**After:**
|
||||
```go
|
||||
result := Ap[string, Context, error, int](value)(funcInContext)
|
||||
// Or better yet, let the compiler infer:
|
||||
result := Ap(value)(funcInContext)
|
||||
```
|
||||
|
||||
### Step 5: Update Pair Operations
|
||||
|
||||
If you're using `Pair`, update operations to work on the second element:
|
||||
|
||||
**Before (V1):**
|
||||
```go
|
||||
pair := MakePair(42, "data")
|
||||
// Map operates on first element
|
||||
result := Map(N.Mul(2))(pair)
|
||||
```
|
||||
|
||||
**After (V2):**
|
||||
```go
|
||||
pair := MakePair(42, "data")
|
||||
// Map operates on second element
|
||||
result := Map(func(s string) string { return s + "!" })(pair)
|
||||
```
|
||||
|
||||
### Step 6: Simplify Type Aliases
|
||||
|
||||
Create project-wide type aliases for common patterns:
|
||||
|
||||
```go
|
||||
func doSth() Either[string] {
|
||||
...
|
||||
// types.go - Define once, use everywhere
|
||||
package myapp
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
)
|
||||
|
||||
type Result[A any] = result.Result[A]
|
||||
type Option[A any] = option.Option[A]
|
||||
type IOResult[A any] = ioresult.IOResult[A]
|
||||
```
|
||||
|
||||
## 🆕 What's New
|
||||
|
||||
### Cleaner API Surface
|
||||
|
||||
The elimination of `generic` subpackages means:
|
||||
- Fewer imports to manage
|
||||
- Simpler package structure
|
||||
- Easier to navigate documentation
|
||||
- More intuitive API
|
||||
|
||||
### Example: Before and After
|
||||
|
||||
**V1 Complex Example:**
|
||||
```go
|
||||
import (
|
||||
ET "github.com/IBM/fp-go/either"
|
||||
EG "github.com/IBM/fp-go/either/generic"
|
||||
IOET "github.com/IBM/fp-go/ioeither"
|
||||
IOEG "github.com/IBM/fp-go/ioeither/generic"
|
||||
)
|
||||
|
||||
func process() IOET.IOEither[error, string] {
|
||||
return IOEG.Map[error, int, string](
|
||||
strconv.Itoa,
|
||||
)(fetchData())
|
||||
}
|
||||
```
|
||||
|
||||
- library implementation does no longer need to use the `generic` subpackage, this simplifies reading and understanding of the code
|
||||
**V2 Simplified Example:**
|
||||
```go
|
||||
import (
|
||||
"strconv"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
)
|
||||
|
||||
type IOResult[A any] = ioresult.IOResult[A]
|
||||
|
||||
func process() IOResult[string] {
|
||||
return ioresult.Map(
|
||||
strconv.Itoa,
|
||||
)(fetchData())
|
||||
}
|
||||
```
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
- **[API Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2)** - Complete API reference
|
||||
- **[Code Samples](./samples/)** - Practical examples and use cases
|
||||
- **[Go 1.24 Release Notes](https://tip.golang.org/doc/go1.24)** - Information about generic type aliases
|
||||
|
||||
### Core Modules
|
||||
|
||||
- **Option** - Represent optional values without nil
|
||||
- **Either** - Type-safe error handling with left/right values
|
||||
- **Result** - Simplified Either with error as left type
|
||||
- **IO** - Lazy evaluation and side effect management
|
||||
- **IOEither** - Combine IO with error handling
|
||||
- **Reader** - Dependency injection pattern
|
||||
- **ReaderIOEither** - Combine Reader, IO, and Either for complex workflows
|
||||
- **Array** - Functional array operations
|
||||
- **Record** - Functional record/map operations
|
||||
- **Optics** - Lens, Prism, Optional, and Traversal for immutable updates
|
||||
|
||||
## 🤔 Should I Migrate?
|
||||
|
||||
**Migrate to V2 if:**
|
||||
- ✅ You can use Go 1.24+
|
||||
- ✅ You want cleaner, more maintainable code
|
||||
- ✅ You want better type inference
|
||||
- ✅ You're starting a new project
|
||||
|
||||
**Stay on V1 if:**
|
||||
- ⚠️ You're locked to Go < 1.24
|
||||
- ⚠️ Migration effort outweighs benefits for your project
|
||||
- ⚠️ You need stability in production (V2 is newer)
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Contributions are welcome! Here's how you can help:
|
||||
|
||||
1. **Report bugs** - Open an issue with a clear description and reproduction steps
|
||||
2. **Suggest features** - Share your ideas for improvements
|
||||
3. **Submit PRs** - Fix bugs or add features (please discuss major changes first)
|
||||
4. **Improve docs** - Help make the documentation clearer and more comprehensive
|
||||
|
||||
Please read our contribution guidelines before submitting pull requests.
|
||||
|
||||
## 🐛 Issues and Feedback
|
||||
|
||||
Found a bug or have a suggestion? Please [open an issue](https://github.com/IBM/fp-go/issues) on GitHub.
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the Apache License 2.0. See the [LICENSE](https://github.com/IBM/fp-go/blob/main/LICENSE) file for details.
|
||||
|
||||
---
|
||||
|
||||
**Made with ❤️ by IBM**
|
||||
@@ -17,11 +17,10 @@ package array
|
||||
|
||||
import (
|
||||
G "github.com/IBM/fp-go/v2/array/generic"
|
||||
EM "github.com/IBM/fp-go/v2/endomorphism"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/array"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/tuple"
|
||||
)
|
||||
|
||||
@@ -50,16 +49,16 @@ func Replicate[A any](n int, a A) []A {
|
||||
// This is the monadic version of Map that takes the array as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadMap[A, B any](as []A, f func(a A) B) []B {
|
||||
func MonadMap[A, B any](as []A, f func(A) B) []B {
|
||||
return G.MonadMap[[]A, []B](as, f)
|
||||
}
|
||||
|
||||
// MonadMapRef applies a function to a pointer to each element of an array, returning a new array with the results.
|
||||
// This is useful when you need to access elements by reference without copying.
|
||||
func MonadMapRef[A, B any](as []A, f func(a *A) B) []B {
|
||||
func MonadMapRef[A, B any](as []A, f func(*A) B) []B {
|
||||
count := len(as)
|
||||
bs := make([]B, count)
|
||||
for i := count - 1; i >= 0; i-- {
|
||||
for i := range count {
|
||||
bs[i] = f(&as[i])
|
||||
}
|
||||
return bs
|
||||
@@ -68,7 +67,7 @@ func MonadMapRef[A, B any](as []A, f func(a *A) B) []B {
|
||||
// MapWithIndex applies a function to each element and its index in an array, returning a new array with the results.
|
||||
//
|
||||
//go:inline
|
||||
func MapWithIndex[A, B any](f func(int, A) B) func([]A) []B {
|
||||
func MapWithIndex[A, B any](f func(int, A) B) Operator[A, B] {
|
||||
return G.MapWithIndex[[]A, []B](f)
|
||||
}
|
||||
|
||||
@@ -77,39 +76,39 @@ func MapWithIndex[A, B any](f func(int, A) B) func([]A) []B {
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// double := array.Map(func(x int) int { return x * 2 })
|
||||
// double := array.Map(N.Mul(2))
|
||||
// result := double([]int{1, 2, 3}) // [2, 4, 6]
|
||||
//
|
||||
//go:inline
|
||||
func Map[A, B any](f func(a A) B) func([]A) []B {
|
||||
return G.Map[[]A, []B, A, B](f)
|
||||
func Map[A, B any](f func(A) B) Operator[A, B] {
|
||||
return G.Map[[]A, []B](f)
|
||||
}
|
||||
|
||||
// MapRef applies a function to a pointer to each element of an array, returning a new array with the results.
|
||||
// This is the curried version that returns a function.
|
||||
func MapRef[A, B any](f func(a *A) B) func([]A) []B {
|
||||
func MapRef[A, B any](f func(*A) B) Operator[A, B] {
|
||||
return F.Bind2nd(MonadMapRef[A, B], f)
|
||||
}
|
||||
|
||||
func filterRef[A any](fa []A, pred func(a *A) bool) []A {
|
||||
var result []A
|
||||
func filterRef[A any](fa []A, pred func(*A) bool) []A {
|
||||
count := len(fa)
|
||||
for i := 0; i < count; i++ {
|
||||
a := fa[i]
|
||||
if pred(&a) {
|
||||
result = append(result, a)
|
||||
var result []A = make([]A, 0, count)
|
||||
for i := range count {
|
||||
a := &fa[i]
|
||||
if pred(a) {
|
||||
result = append(result, *a)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func filterMapRef[A, B any](fa []A, pred func(a *A) bool, f func(a *A) B) []B {
|
||||
var result []B
|
||||
func filterMapRef[A, B any](fa []A, pred func(*A) bool, f func(*A) B) []B {
|
||||
count := len(fa)
|
||||
for i := 0; i < count; i++ {
|
||||
a := fa[i]
|
||||
if pred(&a) {
|
||||
result = append(result, f(&a))
|
||||
var result []B = make([]B, 0, count)
|
||||
for i := range count {
|
||||
a := &fa[i]
|
||||
if pred(a) {
|
||||
result = append(result, f(a))
|
||||
}
|
||||
}
|
||||
return result
|
||||
@@ -118,19 +117,19 @@ func filterMapRef[A, B any](fa []A, pred func(a *A) bool, f func(a *A) B) []B {
|
||||
// Filter returns a new array with all elements from the original array that match a predicate
|
||||
//
|
||||
//go:inline
|
||||
func Filter[A any](pred func(A) bool) EM.Endomorphism[[]A] {
|
||||
func Filter[A any](pred func(A) bool) Operator[A, A] {
|
||||
return G.Filter[[]A](pred)
|
||||
}
|
||||
|
||||
// FilterWithIndex returns a new array with all elements from the original array that match a predicate
|
||||
//
|
||||
//go:inline
|
||||
func FilterWithIndex[A any](pred func(int, A) bool) EM.Endomorphism[[]A] {
|
||||
func FilterWithIndex[A any](pred func(int, A) bool) Operator[A, A] {
|
||||
return G.FilterWithIndex[[]A](pred)
|
||||
}
|
||||
|
||||
// FilterRef returns a new array with all elements from the original array that match a predicate operating on pointers.
|
||||
func FilterRef[A any](pred func(*A) bool) EM.Endomorphism[[]A] {
|
||||
func FilterRef[A any](pred func(*A) bool) Operator[A, A] {
|
||||
return F.Bind2nd(filterRef[A], pred)
|
||||
}
|
||||
|
||||
@@ -138,7 +137,7 @@ func FilterRef[A any](pred func(*A) bool) EM.Endomorphism[[]A] {
|
||||
// This is the monadic version that takes the array as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadFilterMap[A, B any](fa []A, f func(A) O.Option[B]) []B {
|
||||
func MonadFilterMap[A, B any](fa []A, f option.Kleisli[A, B]) []B {
|
||||
return G.MonadFilterMap[[]A, []B](fa, f)
|
||||
}
|
||||
|
||||
@@ -146,33 +145,33 @@ func MonadFilterMap[A, B any](fa []A, f func(A) O.Option[B]) []B {
|
||||
// keeping only the Some values. This is the monadic version that takes the array as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadFilterMapWithIndex[A, B any](fa []A, f func(int, A) O.Option[B]) []B {
|
||||
func MonadFilterMapWithIndex[A, B any](fa []A, f func(int, A) Option[B]) []B {
|
||||
return G.MonadFilterMapWithIndex[[]A, []B](fa, f)
|
||||
}
|
||||
|
||||
// FilterMap maps an array with an iterating function that returns an [O.Option] and it keeps only the Some values discarding the Nones.
|
||||
// FilterMap maps an array with an iterating function that returns an [Option] and it keeps only the Some values discarding the Nones.
|
||||
//
|
||||
//go:inline
|
||||
func FilterMap[A, B any](f func(A) O.Option[B]) func([]A) []B {
|
||||
func FilterMap[A, B any](f option.Kleisli[A, B]) Operator[A, B] {
|
||||
return G.FilterMap[[]A, []B](f)
|
||||
}
|
||||
|
||||
// FilterMapWithIndex maps an array with an iterating function that returns an [O.Option] and it keeps only the Some values discarding the Nones.
|
||||
// FilterMapWithIndex maps an array with an iterating function that returns an [Option] and it keeps only the Some values discarding the Nones.
|
||||
//
|
||||
//go:inline
|
||||
func FilterMapWithIndex[A, B any](f func(int, A) O.Option[B]) func([]A) []B {
|
||||
func FilterMapWithIndex[A, B any](f func(int, A) Option[B]) Operator[A, B] {
|
||||
return G.FilterMapWithIndex[[]A, []B](f)
|
||||
}
|
||||
|
||||
// FilterChain maps an array with an iterating function that returns an [O.Option] of an array. It keeps only the Some values discarding the Nones and then flattens the result.
|
||||
// FilterChain maps an array with an iterating function that returns an [Option] of an array. It keeps only the Some values discarding the Nones and then flattens the result.
|
||||
//
|
||||
//go:inline
|
||||
func FilterChain[A, B any](f func(A) O.Option[[]B]) func([]A) []B {
|
||||
func FilterChain[A, B any](f option.Kleisli[A, []B]) Operator[A, B] {
|
||||
return G.FilterChain[[]A](f)
|
||||
}
|
||||
|
||||
// FilterMapRef filters an array using a predicate on pointers and maps the matching elements using a function on pointers.
|
||||
func FilterMapRef[A, B any](pred func(a *A) bool, f func(a *A) B) func([]A) []B {
|
||||
func FilterMapRef[A, B any](pred func(a *A) bool, f func(*A) B) Operator[A, B] {
|
||||
return func(fa []A) []B {
|
||||
return filterMapRef(fa, pred, f)
|
||||
}
|
||||
@@ -180,8 +179,7 @@ func FilterMapRef[A, B any](pred func(a *A) bool, f func(a *A) B) func([]A) []B
|
||||
|
||||
func reduceRef[A, B any](fa []A, f func(B, *A) B, initial B) B {
|
||||
current := initial
|
||||
count := len(fa)
|
||||
for i := 0; i < count; i++ {
|
||||
for i := range len(fa) {
|
||||
current = f(current, &fa[i])
|
||||
}
|
||||
return current
|
||||
@@ -262,6 +260,8 @@ func Empty[A any]() []A {
|
||||
}
|
||||
|
||||
// Zero returns an empty array of type A (alias for Empty).
|
||||
//
|
||||
//go:inline
|
||||
func Zero[A any]() []A {
|
||||
return Empty[A]()
|
||||
}
|
||||
@@ -277,8 +277,8 @@ func Of[A any](a A) []A {
|
||||
// This is the monadic version that takes the array as the first parameter (also known as FlatMap).
|
||||
//
|
||||
//go:inline
|
||||
func MonadChain[A, B any](fa []A, f func(a A) []B) []B {
|
||||
return G.MonadChain[[]A, []B](fa, f)
|
||||
func MonadChain[A, B any](fa []A, f Kleisli[A, B]) []B {
|
||||
return G.MonadChain(fa, f)
|
||||
}
|
||||
|
||||
// Chain applies a function that returns an array to each element and flattens the results.
|
||||
@@ -290,8 +290,8 @@ func MonadChain[A, B any](fa []A, f func(a A) []B) []B {
|
||||
// result := duplicate([]int{1, 2, 3}) // [1, 1, 2, 2, 3, 3]
|
||||
//
|
||||
//go:inline
|
||||
func Chain[A, B any](f func(A) []B) func([]A) []B {
|
||||
return G.Chain[[]A, []B](f)
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||
return G.Chain[[]A](f)
|
||||
}
|
||||
|
||||
// MonadAp applies an array of functions to an array of values, producing all combinations.
|
||||
@@ -306,7 +306,7 @@ func MonadAp[B, A any](fab []func(A) B, fa []A) []B {
|
||||
// This is the curried version.
|
||||
//
|
||||
//go:inline
|
||||
func Ap[B, A any](fa []A) func([]func(A) B) []B {
|
||||
func Ap[B, A any](fa []A) Operator[func(A) B, B] {
|
||||
return G.Ap[[]B, []func(A) B](fa)
|
||||
}
|
||||
|
||||
@@ -314,21 +314,21 @@ func Ap[B, A any](fa []A) func([]func(A) B) []B {
|
||||
//
|
||||
//go:inline
|
||||
func Match[A, B any](onEmpty func() B, onNonEmpty func([]A) B) func([]A) B {
|
||||
return G.Match[[]A](onEmpty, onNonEmpty)
|
||||
return G.Match(onEmpty, onNonEmpty)
|
||||
}
|
||||
|
||||
// MatchLeft performs pattern matching on an array, calling onEmpty if empty or onNonEmpty with head and tail if not.
|
||||
//
|
||||
//go:inline
|
||||
func MatchLeft[A, B any](onEmpty func() B, onNonEmpty func(A, []A) B) func([]A) B {
|
||||
return G.MatchLeft[[]A](onEmpty, onNonEmpty)
|
||||
return G.MatchLeft(onEmpty, onNonEmpty)
|
||||
}
|
||||
|
||||
// Tail returns all elements except the first, wrapped in an Option.
|
||||
// Returns None if the array is empty.
|
||||
//
|
||||
//go:inline
|
||||
func Tail[A any](as []A) O.Option[[]A] {
|
||||
func Tail[A any](as []A) Option[[]A] {
|
||||
return G.Tail(as)
|
||||
}
|
||||
|
||||
@@ -336,7 +336,7 @@ func Tail[A any](as []A) O.Option[[]A] {
|
||||
// Returns None if the array is empty.
|
||||
//
|
||||
//go:inline
|
||||
func Head[A any](as []A) O.Option[A] {
|
||||
func Head[A any](as []A) Option[A] {
|
||||
return G.Head(as)
|
||||
}
|
||||
|
||||
@@ -344,7 +344,7 @@ func Head[A any](as []A) O.Option[A] {
|
||||
// Returns None if the array is empty.
|
||||
//
|
||||
//go:inline
|
||||
func First[A any](as []A) O.Option[A] {
|
||||
func First[A any](as []A) Option[A] {
|
||||
return G.First(as)
|
||||
}
|
||||
|
||||
@@ -352,12 +352,12 @@ func First[A any](as []A) O.Option[A] {
|
||||
// Returns None if the array is empty.
|
||||
//
|
||||
//go:inline
|
||||
func Last[A any](as []A) O.Option[A] {
|
||||
func Last[A any](as []A) Option[A] {
|
||||
return G.Last(as)
|
||||
}
|
||||
|
||||
// PrependAll inserts a separator before each element of an array.
|
||||
func PrependAll[A any](middle A) EM.Endomorphism[[]A] {
|
||||
func PrependAll[A any](middle A) Operator[A, A] {
|
||||
return func(as []A) []A {
|
||||
count := len(as)
|
||||
dst := count * 2
|
||||
@@ -377,7 +377,7 @@ func PrependAll[A any](middle A) EM.Endomorphism[[]A] {
|
||||
// Example:
|
||||
//
|
||||
// result := array.Intersperse(0)([]int{1, 2, 3}) // [1, 0, 2, 0, 3]
|
||||
func Intersperse[A any](middle A) EM.Endomorphism[[]A] {
|
||||
func Intersperse[A any](middle A) Operator[A, A] {
|
||||
prepend := PrependAll(middle)
|
||||
return func(as []A) []A {
|
||||
if IsEmpty(as) {
|
||||
@@ -390,7 +390,7 @@ func Intersperse[A any](middle A) EM.Endomorphism[[]A] {
|
||||
// Intercalate inserts a separator between elements and concatenates them using a Monoid.
|
||||
func Intercalate[A any](m M.Monoid[A]) func(A) func([]A) A {
|
||||
return func(middle A) func([]A) A {
|
||||
return Match(m.Empty, F.Flow2(Intersperse(middle), ConcatAll[A](m)))
|
||||
return Match(m.Empty, F.Flow2(Intersperse(middle), ConcatAll(m)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,7 +406,7 @@ func Flatten[A any](mma [][]A) []A {
|
||||
}
|
||||
|
||||
// Slice extracts a subarray from index low (inclusive) to high (exclusive).
|
||||
func Slice[A any](low, high int) func(as []A) []A {
|
||||
func Slice[A any](low, high int) Operator[A, A] {
|
||||
return array.Slice[[]A](low, high)
|
||||
}
|
||||
|
||||
@@ -414,7 +414,7 @@ func Slice[A any](low, high int) func(as []A) []A {
|
||||
// Returns None if the index is out of bounds.
|
||||
//
|
||||
//go:inline
|
||||
func Lookup[A any](idx int) func([]A) O.Option[A] {
|
||||
func Lookup[A any](idx int) func([]A) Option[A] {
|
||||
return G.Lookup[[]A](idx)
|
||||
}
|
||||
|
||||
@@ -422,7 +422,7 @@ func Lookup[A any](idx int) func([]A) O.Option[A] {
|
||||
// If the index is out of bounds, the element is appended.
|
||||
//
|
||||
//go:inline
|
||||
func UpsertAt[A any](a A) EM.Endomorphism[[]A] {
|
||||
func UpsertAt[A any](a A) Operator[A, A] {
|
||||
return G.UpsertAt[[]A](a)
|
||||
}
|
||||
|
||||
@@ -468,7 +468,7 @@ func ConstNil[A any]() []A {
|
||||
// SliceRight extracts a subarray from the specified start index to the end.
|
||||
//
|
||||
//go:inline
|
||||
func SliceRight[A any](start int) EM.Endomorphism[[]A] {
|
||||
func SliceRight[A any](start int) Operator[A, A] {
|
||||
return G.SliceRight[[]A](start)
|
||||
}
|
||||
|
||||
@@ -482,7 +482,7 @@ func Copy[A any](b []A) []A {
|
||||
// Clone creates a deep copy of the array using the provided endomorphism to clone the values
|
||||
//
|
||||
//go:inline
|
||||
func Clone[A any](f func(A) A) func(as []A) []A {
|
||||
func Clone[A any](f func(A) A) Operator[A, A] {
|
||||
return G.Clone[[]A](f)
|
||||
}
|
||||
|
||||
@@ -510,8 +510,8 @@ func Fold[A any](m M.Monoid[A]) func([]A) A {
|
||||
// Push adds an element to the end of an array (alias for Append).
|
||||
//
|
||||
//go:inline
|
||||
func Push[A any](a A) EM.Endomorphism[[]A] {
|
||||
return G.Push[EM.Endomorphism[[]A]](a)
|
||||
func Push[A any](a A) Operator[A, A] {
|
||||
return G.Push[Operator[A, A]](a)
|
||||
}
|
||||
|
||||
// MonadFlap applies a value to an array of functions, producing an array of results.
|
||||
@@ -519,20 +519,20 @@ func Push[A any](a A) EM.Endomorphism[[]A] {
|
||||
//
|
||||
//go:inline
|
||||
func MonadFlap[B, A any](fab []func(A) B, a A) []B {
|
||||
return G.MonadFlap[func(A) B, []func(A) B, []B, A, B](fab, a)
|
||||
return G.MonadFlap[func(A) B, []func(A) B, []B](fab, a)
|
||||
}
|
||||
|
||||
// Flap applies a value to an array of functions, producing an array of results.
|
||||
// This is the curried version.
|
||||
//
|
||||
//go:inline
|
||||
func Flap[B, A any](a A) func([]func(A) B) []B {
|
||||
return G.Flap[func(A) B, []func(A) B, []B, A, B](a)
|
||||
func Flap[B, A any](a A) Operator[func(A) B, B] {
|
||||
return G.Flap[func(A) B, []func(A) B, []B](a)
|
||||
}
|
||||
|
||||
// Prepend adds an element to the beginning of an array, returning a new array.
|
||||
//
|
||||
//go:inline
|
||||
func Prepend[A any](head A) EM.Endomorphism[[]A] {
|
||||
return G.Prepend[EM.Endomorphism[[]A]](head)
|
||||
func Prepend[A any](head A) Operator[A, A] {
|
||||
return G.Prepend[Operator[A, A]](head)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ func TestAp(t *testing.T) {
|
||||
utils.Double,
|
||||
utils.Triple,
|
||||
},
|
||||
Ap[int, int]([]int{1, 2, 3}),
|
||||
Ap[int]([]int{1, 2, 3}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ import (
|
||||
func Do[S any](
|
||||
empty S,
|
||||
) []S {
|
||||
return G.Do[[]S, S](empty)
|
||||
return G.Do[[]S](empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context S1 to produce a context S2.
|
||||
@@ -56,9 +56,9 @@ func Do[S any](
|
||||
//go:inline
|
||||
func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) []T,
|
||||
) func([]S1) []S2 {
|
||||
return G.Bind[[]S1, []S2, []T, S1, S2, T](setter, f)
|
||||
f Kleisli[S1, T],
|
||||
) Operator[S1, S2] {
|
||||
return G.Bind[[]S1, []S2](setter, f)
|
||||
}
|
||||
|
||||
// Let attaches the result of a pure computation to a context S1 to produce a context S2.
|
||||
@@ -79,8 +79,8 @@ func Bind[S1, S2, T any](
|
||||
func Let[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
) func([]S1) []S2 {
|
||||
return G.Let[[]S1, []S2, S1, S2, T](setter, f)
|
||||
) Operator[S1, S2] {
|
||||
return G.Let[[]S1, []S2](setter, f)
|
||||
}
|
||||
|
||||
// LetTo attaches a constant value to a context S1 to produce a context S2.
|
||||
@@ -101,8 +101,8 @@ func Let[S1, S2, T any](
|
||||
func LetTo[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
) func([]S1) []S2 {
|
||||
return G.LetTo[[]S1, []S2, S1, S2, T](setter, b)
|
||||
) Operator[S1, S2] {
|
||||
return G.LetTo[[]S1, []S2](setter, b)
|
||||
}
|
||||
|
||||
// BindTo initializes a new state S1 from a value T.
|
||||
@@ -120,8 +120,8 @@ func LetTo[S1, S2, T any](
|
||||
//go:inline
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
) func([]T) []S1 {
|
||||
return G.BindTo[[]S1, []T, S1, T](setter)
|
||||
) Operator[T, S1] {
|
||||
return G.BindTo[[]S1, []T](setter)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context S1 to produce a context S2 by considering
|
||||
@@ -143,6 +143,6 @@ func BindTo[S1, T any](
|
||||
func ApS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa []T,
|
||||
) func([]S1) []S2 {
|
||||
return G.ApS[[]S1, []S2, []T, S1, S2, T](setter, fa)
|
||||
) Operator[S1, S2] {
|
||||
return G.ApS[[]S1, []S2](setter, fa)
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
// generated := array.MakeBy(5, func(i int) int { return i * 2 })
|
||||
//
|
||||
// // Transforming arrays
|
||||
// doubled := array.Map(func(x int) int { return x * 2 })(arr)
|
||||
// doubled := array.Map(N.Mul(2))(arr)
|
||||
// filtered := array.Filter(func(x int) bool { return x > 2 })(arr)
|
||||
//
|
||||
// // Combining arrays
|
||||
@@ -50,7 +50,7 @@
|
||||
// numbers := []int{1, 2, 3, 4, 5}
|
||||
//
|
||||
// // Map transforms each element
|
||||
// doubled := array.Map(func(x int) int { return x * 2 })(numbers)
|
||||
// doubled := array.Map(N.Mul(2))(numbers)
|
||||
// // Result: [2, 4, 6, 8, 10]
|
||||
//
|
||||
// // Filter keeps elements matching a predicate
|
||||
|
||||
@@ -87,6 +87,6 @@ func Example_sort() {
|
||||
// [abc klm zyx]
|
||||
// [zyx klm abc]
|
||||
// [None[int] Some[int](42) Some[int](1337)]
|
||||
// [{c {false 0}} {b {true 10}} {d {true 10}} {a {true 30}}]
|
||||
// [{c {0 false}} {b {10 true}} {d {10 true}} {a {30 true}}]
|
||||
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ package array
|
||||
|
||||
import (
|
||||
G "github.com/IBM/fp-go/v2/array/generic"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// FindFirst finds the first element which satisfies a predicate function.
|
||||
@@ -30,7 +30,7 @@ import (
|
||||
// result2 := findGreaterThan3([]int{1, 2, 3}) // None
|
||||
//
|
||||
//go:inline
|
||||
func FindFirst[A any](pred func(A) bool) func([]A) O.Option[A] {
|
||||
func FindFirst[A any](pred func(A) bool) option.Kleisli[[]A, A] {
|
||||
return G.FindFirst[[]A](pred)
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ func FindFirst[A any](pred func(A) bool) func([]A) O.Option[A] {
|
||||
// result := findEvenAtEvenIndex([]int{1, 3, 4, 5}) // Some(4)
|
||||
//
|
||||
//go:inline
|
||||
func FindFirstWithIndex[A any](pred func(int, A) bool) func([]A) O.Option[A] {
|
||||
func FindFirstWithIndex[A any](pred func(int, A) bool) option.Kleisli[[]A, A] {
|
||||
return G.FindFirstWithIndex[[]A](pred)
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ func FindFirstWithIndex[A any](pred func(int, A) bool) func([]A) O.Option[A] {
|
||||
// result := parseFirst([]string{"a", "42", "b"}) // Some(42)
|
||||
//
|
||||
//go:inline
|
||||
func FindFirstMap[A, B any](sel func(A) O.Option[B]) func([]A) O.Option[B] {
|
||||
func FindFirstMap[A, B any](sel option.Kleisli[A, B]) option.Kleisli[[]A, B] {
|
||||
return G.FindFirstMap[[]A](sel)
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ func FindFirstMap[A, B any](sel func(A) O.Option[B]) func([]A) O.Option[B] {
|
||||
// The selector receives both the index and the element.
|
||||
//
|
||||
//go:inline
|
||||
func FindFirstMapWithIndex[A, B any](sel func(int, A) O.Option[B]) func([]A) O.Option[B] {
|
||||
func FindFirstMapWithIndex[A, B any](sel func(int, A) Option[B]) option.Kleisli[[]A, B] {
|
||||
return G.FindFirstMapWithIndex[[]A](sel)
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ func FindFirstMapWithIndex[A, B any](sel func(int, A) O.Option[B]) func([]A) O.O
|
||||
// result := findGreaterThan3([]int{1, 4, 2, 5}) // Some(5)
|
||||
//
|
||||
//go:inline
|
||||
func FindLast[A any](pred func(A) bool) func([]A) O.Option[A] {
|
||||
func FindLast[A any](pred func(A) bool) option.Kleisli[[]A, A] {
|
||||
return G.FindLast[[]A](pred)
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ func FindLast[A any](pred func(A) bool) func([]A) O.Option[A] {
|
||||
// Returns Some(element) if found, None if no element matches.
|
||||
//
|
||||
//go:inline
|
||||
func FindLastWithIndex[A any](pred func(int, A) bool) func([]A) O.Option[A] {
|
||||
func FindLastWithIndex[A any](pred func(int, A) bool) option.Kleisli[[]A, A] {
|
||||
return G.FindLastWithIndex[[]A](pred)
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ func FindLastWithIndex[A any](pred func(int, A) bool) func([]A) O.Option[A] {
|
||||
// This combines finding and mapping in a single operation, searching from the end.
|
||||
//
|
||||
//go:inline
|
||||
func FindLastMap[A, B any](sel func(A) O.Option[B]) func([]A) O.Option[B] {
|
||||
func FindLastMap[A, B any](sel option.Kleisli[A, B]) option.Kleisli[[]A, B] {
|
||||
return G.FindLastMap[[]A](sel)
|
||||
}
|
||||
|
||||
@@ -110,6 +110,6 @@ func FindLastMap[A, B any](sel func(A) O.Option[B]) func([]A) O.Option[B] {
|
||||
// The selector receives both the index and the element, searching from the end.
|
||||
//
|
||||
//go:inline
|
||||
func FindLastMapWithIndex[A, B any](sel func(int, A) O.Option[B]) func([]A) O.Option[B] {
|
||||
func FindLastMapWithIndex[A, B any](sel func(int, A) Option[B]) option.Kleisli[[]A, B] {
|
||||
return G.FindLastMapWithIndex[[]A](sel)
|
||||
}
|
||||
|
||||
@@ -25,31 +25,33 @@ import (
|
||||
)
|
||||
|
||||
// Of constructs a single element array
|
||||
//
|
||||
//go:inline
|
||||
func Of[GA ~[]A, A any](value A) GA {
|
||||
return GA{value}
|
||||
return array.Of[GA](value)
|
||||
}
|
||||
|
||||
func Reduce[GA ~[]A, A, B any](f func(B, A) B, initial B) func(GA) B {
|
||||
return func(as GA) B {
|
||||
return MonadReduce[GA](as, f, initial)
|
||||
return MonadReduce(as, f, initial)
|
||||
}
|
||||
}
|
||||
|
||||
func ReduceWithIndex[GA ~[]A, A, B any](f func(int, B, A) B, initial B) func(GA) B {
|
||||
return func(as GA) B {
|
||||
return MonadReduceWithIndex[GA](as, f, initial)
|
||||
return MonadReduceWithIndex(as, f, initial)
|
||||
}
|
||||
}
|
||||
|
||||
func ReduceRight[GA ~[]A, A, B any](f func(A, B) B, initial B) func(GA) B {
|
||||
return func(as GA) B {
|
||||
return MonadReduceRight[GA](as, f, initial)
|
||||
return MonadReduceRight(as, f, initial)
|
||||
}
|
||||
}
|
||||
|
||||
func ReduceRightWithIndex[GA ~[]A, A, B any](f func(int, A, B) B, initial B) func(GA) B {
|
||||
return func(as GA) B {
|
||||
return MonadReduceRightWithIndex[GA](as, f, initial)
|
||||
return MonadReduceRightWithIndex(as, f, initial)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +84,7 @@ func MakeBy[AS ~[]A, F ~func(int) A, A any](n int, f F) AS {
|
||||
}
|
||||
// run the generator function across the input
|
||||
as := make(AS, n)
|
||||
for i := n - 1; i >= 0; i-- {
|
||||
for i := range n {
|
||||
as[i] = f(i)
|
||||
}
|
||||
return as
|
||||
@@ -165,10 +167,9 @@ func Size[GA ~[]A, A any](as GA) int {
|
||||
func filterMap[GA ~[]A, GB ~[]B, A, B any](fa GA, f func(A) O.Option[B]) GB {
|
||||
result := make(GB, 0, len(fa))
|
||||
for _, a := range fa {
|
||||
O.Map(func(b B) B {
|
||||
if b, ok := O.Unwrap(f(a)); ok {
|
||||
result = append(result, b)
|
||||
return b
|
||||
})(f(a))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -176,10 +177,9 @@ func filterMap[GA ~[]A, GB ~[]B, A, B any](fa GA, f func(A) O.Option[B]) GB {
|
||||
func filterMapWithIndex[GA ~[]A, GB ~[]B, A, B any](fa GA, f func(int, A) O.Option[B]) GB {
|
||||
result := make(GB, 0, len(fa))
|
||||
for i, a := range fa {
|
||||
O.Map(func(b B) B {
|
||||
if b, ok := O.Unwrap(f(i, a)); ok {
|
||||
result = append(result, b)
|
||||
return b
|
||||
})(f(i, a))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -21,14 +21,56 @@ import (
|
||||
F "github.com/IBM/fp-go/v2/internal/functor"
|
||||
)
|
||||
|
||||
// Bind creates an empty context of type [S] to be used with the [Bind] operation
|
||||
// Do creates an empty context of type [S] to be used with the [Bind] operation.
|
||||
// This is the starting point for do-notation style composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// }
|
||||
// result := generic.Do[[]State, State](State{})
|
||||
func Do[GS ~[]S, S any](
|
||||
empty S,
|
||||
) GS {
|
||||
return Of[GS](empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// This enables sequential composition where each step can depend on the results of previous steps.
|
||||
// For arrays, this produces the cartesian product where later steps can use values from earlier steps.
|
||||
//
|
||||
// The setter function takes the result of the computation and returns a function that
|
||||
// updates the context from S1 to S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// generic.Do[[]State, State](State{}),
|
||||
// generic.Bind[[]State, []State, []int, State, State, int](
|
||||
// func(x int) func(State) State {
|
||||
// return func(s State) State { s.X = x; return s }
|
||||
// },
|
||||
// func(s State) []int {
|
||||
// return []int{1, 2, 3}
|
||||
// },
|
||||
// ),
|
||||
// generic.Bind[[]State, []State, []int, State, State, int](
|
||||
// func(y int) func(State) State {
|
||||
// return func(s State) State { s.Y = y; return s }
|
||||
// },
|
||||
// func(s State) []int {
|
||||
// // This can access s.X from the previous step
|
||||
// return []int{s.X * 10, s.X * 20}
|
||||
// },
|
||||
// ),
|
||||
// ) // Produces: {1,10}, {1,20}, {2,20}, {2,40}, {3,30}, {3,60}
|
||||
func Bind[GS1 ~[]S1, GS2 ~[]S2, GT ~[]T, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) GT,
|
||||
@@ -75,7 +117,39 @@ func BindTo[GS1 ~[]S1, GT ~[]T, S1, T any](
|
||||
)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
// the context and the value concurrently (using Applicative rather than Monad).
|
||||
// This allows independent computations to be combined without one depending on the result of the other.
|
||||
//
|
||||
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
|
||||
// and can conceptually run in parallel. For arrays, this produces the cartesian product.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y string
|
||||
// }
|
||||
//
|
||||
// // These operations are independent and can be combined with ApS
|
||||
// xValues := []int{1, 2}
|
||||
// yValues := []string{"a", "b"}
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// generic.Do[[]State, State](State{}),
|
||||
// generic.ApS[[]State, []State, []int, State, State, int](
|
||||
// func(x int) func(State) State {
|
||||
// return func(s State) State { s.X = x; return s }
|
||||
// },
|
||||
// xValues,
|
||||
// ),
|
||||
// generic.ApS[[]State, []State, []string, State, State, string](
|
||||
// func(y string) func(State) State {
|
||||
// return func(s State) State { s.Y = y; return s }
|
||||
// },
|
||||
// yValues,
|
||||
// ),
|
||||
// ) // [{1,"a"}, {1,"b"}, {2,"a"}, {2,"b"}]
|
||||
func ApS[GS1 ~[]S1, GS2 ~[]S2, GT ~[]T, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa GT,
|
||||
|
||||
@@ -42,8 +42,7 @@ func FindFirst[AS ~[]A, PRED ~func(A) bool, A any](pred PRED) func(AS) O.Option[
|
||||
func FindFirstMapWithIndex[AS ~[]A, PRED ~func(int, A) O.Option[B], A, B any](pred PRED) func(AS) O.Option[B] {
|
||||
none := O.None[B]()
|
||||
return func(as AS) O.Option[B] {
|
||||
count := len(as)
|
||||
for i := 0; i < count; i++ {
|
||||
for i := range len(as) {
|
||||
out := pred(i, as[i])
|
||||
if O.IsSome(out) {
|
||||
return out
|
||||
|
||||
@@ -22,19 +22,19 @@ import (
|
||||
type arrayMonad[A, B any, GA ~[]A, GB ~[]B, GAB ~[]func(A) B] struct{}
|
||||
|
||||
func (o *arrayMonad[A, B, GA, GB, GAB]) Of(a A) GA {
|
||||
return Of[GA, A](a)
|
||||
return Of[GA](a)
|
||||
}
|
||||
|
||||
func (o *arrayMonad[A, B, GA, GB, GAB]) Map(f func(A) B) func(GA) GB {
|
||||
return Map[GA, GB, A, B](f)
|
||||
return Map[GA, GB](f)
|
||||
}
|
||||
|
||||
func (o *arrayMonad[A, B, GA, GB, GAB]) Chain(f func(A) GB) func(GA) GB {
|
||||
return Chain[GA, GB, A, B](f)
|
||||
return Chain[GA](f)
|
||||
}
|
||||
|
||||
func (o *arrayMonad[A, B, GA, GB, GAB]) Ap(fa GA) func(GAB) GB {
|
||||
return Ap[GB, GAB, GA, B, A](fa)
|
||||
return Ap[GB, GAB](fa)
|
||||
}
|
||||
|
||||
// Monad implements the monadic operations for an array
|
||||
|
||||
34
v2/array/generic/monoid.go
Normal file
34
v2/array/generic/monoid.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/internal/array"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
)
|
||||
|
||||
// Monoid returns a Monoid instance for arrays.
|
||||
// The Monoid combines arrays through concatenation, with an empty array as the identity element.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// m := array.Monoid[int]()
|
||||
// result := m.Concat([]int{1, 2}, []int{3, 4}) // [1, 2, 3, 4]
|
||||
// empty := m.Empty() // []
|
||||
//
|
||||
//go:inline
|
||||
func Monoid[GT ~[]T, T any]() M.Monoid[GT] {
|
||||
return M.MakeMonoid(array.Concat[GT], Empty[GT]())
|
||||
}
|
||||
|
||||
// Semigroup returns a Semigroup instance for arrays.
|
||||
// The Semigroup combines arrays through concatenation.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// s := array.Semigroup[int]()
|
||||
// result := s.Concat([]int{1, 2}, []int{3, 4}) // [1, 2, 3, 4]
|
||||
//
|
||||
//go:inline
|
||||
func Semigroup[GT ~[]T, T any]() S.Semigroup[GT] {
|
||||
return S.MakeSemigroup(array.Concat[GT])
|
||||
}
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
func ZipWith[AS ~[]A, BS ~[]B, CS ~[]C, FCT ~func(A, B) C, A, B, C any](fa AS, fb BS, f FCT) CS {
|
||||
l := N.Min(len(fa), len(fb))
|
||||
res := make(CS, l)
|
||||
for i := l - 1; i >= 0; i-- {
|
||||
for i := range l {
|
||||
res[i] = f(fa[i], fb[i])
|
||||
}
|
||||
return res
|
||||
@@ -43,7 +43,7 @@ func Unzip[AS ~[]A, BS ~[]B, CS ~[]T.Tuple2[A, B], A, B any](cs CS) T.Tuple2[AS,
|
||||
l := len(cs)
|
||||
as := make(AS, l)
|
||||
bs := make(BS, l)
|
||||
for i := l - 1; i >= 0; i-- {
|
||||
for i := range l {
|
||||
t := cs[i]
|
||||
as[i] = t.F1
|
||||
bs[i] = t.F2
|
||||
|
||||
@@ -18,7 +18,6 @@ package array
|
||||
import (
|
||||
"testing"
|
||||
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
OR "github.com/IBM/fp-go/v2/ord"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -103,39 +102,6 @@ func TestSortByKey(t *testing.T) {
|
||||
assert.Equal(t, "Charlie", result[2].Name)
|
||||
}
|
||||
|
||||
func TestMonadTraverse(t *testing.T) {
|
||||
result := MonadTraverse(
|
||||
O.Of[[]int],
|
||||
O.Map[[]int, func(int) []int],
|
||||
O.Ap[[]int, int],
|
||||
[]int{1, 3, 5},
|
||||
func(n int) O.Option[int] {
|
||||
if n%2 == 1 {
|
||||
return O.Some(n * 2)
|
||||
}
|
||||
return O.None[int]()
|
||||
},
|
||||
)
|
||||
|
||||
assert.Equal(t, O.Some([]int{2, 6, 10}), result)
|
||||
|
||||
// Test with None case
|
||||
result2 := MonadTraverse(
|
||||
O.Of[[]int],
|
||||
O.Map[[]int, func(int) []int],
|
||||
O.Ap[[]int, int],
|
||||
[]int{1, 2, 3},
|
||||
func(n int) O.Option[int] {
|
||||
if n%2 == 1 {
|
||||
return O.Some(n * 2)
|
||||
}
|
||||
return O.None[int]()
|
||||
},
|
||||
)
|
||||
|
||||
assert.Equal(t, O.None[[]int](), result2)
|
||||
}
|
||||
|
||||
func TestUniqByKey(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
|
||||
@@ -16,27 +16,12 @@
|
||||
package array
|
||||
|
||||
import (
|
||||
G "github.com/IBM/fp-go/v2/array/generic"
|
||||
"github.com/IBM/fp-go/v2/internal/array"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
)
|
||||
|
||||
func concat[T any](left, right []T) []T {
|
||||
// some performance checks
|
||||
ll := len(left)
|
||||
if ll == 0 {
|
||||
return right
|
||||
}
|
||||
lr := len(right)
|
||||
if lr == 0 {
|
||||
return left
|
||||
}
|
||||
// need to copy
|
||||
buf := make([]T, ll+lr)
|
||||
copy(buf[copy(buf, left):], right)
|
||||
return buf
|
||||
}
|
||||
|
||||
// Monoid returns a Monoid instance for arrays.
|
||||
// The Monoid combines arrays through concatenation, with an empty array as the identity element.
|
||||
//
|
||||
@@ -45,8 +30,10 @@ func concat[T any](left, right []T) []T {
|
||||
// m := array.Monoid[int]()
|
||||
// result := m.Concat([]int{1, 2}, []int{3, 4}) // [1, 2, 3, 4]
|
||||
// empty := m.Empty() // []
|
||||
//
|
||||
//go:inline
|
||||
func Monoid[T any]() M.Monoid[[]T] {
|
||||
return M.MakeMonoid(concat[T], Empty[T]())
|
||||
return G.Monoid[[]T]()
|
||||
}
|
||||
|
||||
// Semigroup returns a Semigroup instance for arrays.
|
||||
@@ -56,8 +43,10 @@ func Monoid[T any]() M.Monoid[[]T] {
|
||||
//
|
||||
// s := array.Semigroup[int]()
|
||||
// result := s.Concat([]int{1, 2}, []int{3, 4}) // [1, 2, 3, 4]
|
||||
//
|
||||
//go:inline
|
||||
func Semigroup[T any]() S.Semigroup[[]T] {
|
||||
return S.MakeSemigroup(concat[T])
|
||||
return G.Semigroup[[]T]()
|
||||
}
|
||||
|
||||
func addLen[A any](count int, data []A) int {
|
||||
|
||||
@@ -97,11 +97,11 @@ func Flatten[A any](mma NonEmptyArray[NonEmptyArray[A]]) NonEmptyArray[A] {
|
||||
}
|
||||
|
||||
func MonadChain[A, B any](fa NonEmptyArray[A], f func(a A) NonEmptyArray[B]) NonEmptyArray[B] {
|
||||
return G.MonadChain[NonEmptyArray[A], NonEmptyArray[B]](fa, f)
|
||||
return G.MonadChain(fa, f)
|
||||
}
|
||||
|
||||
func Chain[A, B any](f func(A) NonEmptyArray[B]) func(NonEmptyArray[A]) NonEmptyArray[B] {
|
||||
return G.Chain[NonEmptyArray[A], NonEmptyArray[B]](f)
|
||||
return G.Chain[NonEmptyArray[A]](f)
|
||||
}
|
||||
|
||||
func MonadAp[B, A any](fab NonEmptyArray[func(A) B], fa NonEmptyArray[A]) NonEmptyArray[B] {
|
||||
|
||||
@@ -16,10 +16,18 @@
|
||||
package array
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/array"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
func MonadSequence[HKTA, HKTRA any](
|
||||
fof func(HKTA) HKTRA,
|
||||
m M.Monoid[HKTRA],
|
||||
ma []HKTA) HKTRA {
|
||||
return array.MonadSequence(fof, m.Empty(), m.Concat, ma)
|
||||
}
|
||||
|
||||
// Sequence takes an array where elements are HKT<A> (higher kinded type) and,
|
||||
// using an applicative of that HKT, returns an HKT of []A.
|
||||
//
|
||||
@@ -55,16 +63,11 @@ import (
|
||||
// option.MonadAp[[]int, int],
|
||||
// )
|
||||
// result := seq(opts) // Some([1, 2, 3])
|
||||
func Sequence[A, HKTA, HKTRA, HKTFRA any](
|
||||
_of func([]A) HKTRA,
|
||||
_map func(HKTRA, func([]A) func(A) []A) HKTFRA,
|
||||
_ap func(HKTFRA, HKTA) HKTRA,
|
||||
func Sequence[HKTA, HKTRA any](
|
||||
fof func(HKTA) HKTRA,
|
||||
m M.Monoid[HKTRA],
|
||||
) func([]HKTA) HKTRA {
|
||||
ca := F.Curry2(Append[A])
|
||||
empty := _of(Empty[A]())
|
||||
return Reduce(func(fas HKTRA, fa HKTA) HKTRA {
|
||||
return _ap(_map(fas, ca), fa)
|
||||
}, empty)
|
||||
return array.Sequence[[]HKTA](fof, m.Empty(), m.Concat)
|
||||
}
|
||||
|
||||
// ArrayOption returns a function to convert a sequence of options into an option of a sequence.
|
||||
@@ -86,10 +89,10 @@ func Sequence[A, HKTA, HKTRA, HKTFRA any](
|
||||
// option.Some(3),
|
||||
// }
|
||||
// result2 := array.ArrayOption[int]()(opts2) // None
|
||||
func ArrayOption[A any]() func([]O.Option[A]) O.Option[[]A] {
|
||||
return Sequence(
|
||||
O.Of[[]A],
|
||||
O.MonadMap[[]A, func(A) []A],
|
||||
O.MonadAp[[]A, A],
|
||||
func ArrayOption[A any](ma []Option[A]) Option[[]A] {
|
||||
return MonadSequence(
|
||||
O.Map(Of[A]),
|
||||
O.ApplicativeMonoid(Monoid[A]()),
|
||||
ma,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,8 +24,7 @@ import (
|
||||
)
|
||||
|
||||
func TestSequenceOption(t *testing.T) {
|
||||
seq := ArrayOption[int]()
|
||||
|
||||
assert.Equal(t, O.Of([]int{1, 3}), seq([]O.Option[int]{O.Of(1), O.Of(3)}))
|
||||
assert.Equal(t, O.None[[]int](), seq([]O.Option[int]{O.Of(1), O.None[int]()}))
|
||||
assert.Equal(t, O.Of([]int{1, 3}), ArrayOption([]O.Option[int]{O.Of(1), O.Of(3)}))
|
||||
assert.Equal(t, O.None[[]int](), ArrayOption([]O.Option[int]{O.Of(1), O.None[int]()}))
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package array
|
||||
import (
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -243,7 +244,7 @@ func TestSliceComposition(t *testing.T) {
|
||||
|
||||
t.Run("slice then map", func(t *testing.T) {
|
||||
sliced := Slice[int](2, 5)(data)
|
||||
mapped := Map(func(x int) int { return x * 2 })(sliced)
|
||||
mapped := Map(N.Mul(2))(sliced)
|
||||
assert.Equal(t, []int{4, 6, 8}, mapped)
|
||||
})
|
||||
|
||||
@@ -403,5 +404,3 @@ func TestSlicePropertyBased(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
|
||||
@@ -32,7 +32,7 @@ import (
|
||||
// // Result: [1, 1, 2, 3, 4, 5, 6, 9]
|
||||
//
|
||||
//go:inline
|
||||
func Sort[T any](ord O.Ord[T]) func(ma []T) []T {
|
||||
func Sort[T any](ord O.Ord[T]) Operator[T, T] {
|
||||
return G.Sort[[]T](ord)
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ func Sort[T any](ord O.Ord[T]) func(ma []T) []T {
|
||||
// // Result: [{"Bob", 25}, {"Alice", 30}, {"Charlie", 35}]
|
||||
//
|
||||
//go:inline
|
||||
func SortByKey[K, T any](ord O.Ord[K], f func(T) K) func(ma []T) []T {
|
||||
func SortByKey[K, T any](ord O.Ord[K], f func(T) K) Operator[T, T] {
|
||||
return G.SortByKey[[]T](ord, f)
|
||||
}
|
||||
|
||||
@@ -93,6 +93,6 @@ func SortByKey[K, T any](ord O.Ord[K], f func(T) K) func(ma []T) []T {
|
||||
// // Result: [{"Jones", "Bob"}, {"Smith", "Alice"}, {"Smith", "John"}]
|
||||
//
|
||||
//go:inline
|
||||
func SortBy[T any](ord []O.Ord[T]) func(ma []T) []T {
|
||||
return G.SortBy[[]T, []O.Ord[T]](ord)
|
||||
func SortBy[T any](ord []O.Ord[T]) Operator[T, T] {
|
||||
return G.SortBy[[]T](ord)
|
||||
}
|
||||
|
||||
@@ -80,3 +80,25 @@ func MonadTraverse[A, B, HKTB, HKTAB, HKTRB any](
|
||||
|
||||
return array.MonadTraverse(fof, fmap, fap, ta, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TraverseWithIndex[A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func([]B) HKTRB,
|
||||
fmap func(func([]B) func(B) []B) func(HKTRB) HKTAB,
|
||||
fap func(HKTB) func(HKTAB) HKTRB,
|
||||
|
||||
f func(int, A) HKTB) func([]A) HKTRB {
|
||||
return array.TraverseWithIndex[[]A](fof, fmap, fap, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadTraverseWithIndex[A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func([]B) HKTRB,
|
||||
fmap func(func([]B) func(B) []B) func(HKTRB) HKTAB,
|
||||
fap func(HKTB) func(HKTAB) HKTRB,
|
||||
|
||||
ta []A,
|
||||
f func(int, A) HKTB) HKTRB {
|
||||
|
||||
return array.MonadTraverseWithIndex(fof, fmap, fap, ta, f)
|
||||
}
|
||||
|
||||
9
v2/array/types.go
Normal file
9
v2/array/types.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package array
|
||||
|
||||
import "github.com/IBM/fp-go/v2/option"
|
||||
|
||||
type (
|
||||
Kleisli[A, B any] = func(A) []B
|
||||
Operator[A, B any] = Kleisli[[]A, B]
|
||||
Option[A any] = option.Option[A]
|
||||
)
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
//
|
||||
//go:inline
|
||||
func StrictUniq[A comparable](as []A) []A {
|
||||
return G.StrictUniq[[]A](as)
|
||||
return G.StrictUniq(as)
|
||||
}
|
||||
|
||||
// Uniq converts an array of arbitrary items into an array of unique items
|
||||
@@ -46,6 +46,6 @@ func StrictUniq[A comparable](as []A) []A {
|
||||
// // Result: [{"Alice", 30}, {"Bob", 25}, {"Charlie", 30}]
|
||||
//
|
||||
//go:inline
|
||||
func Uniq[A any, K comparable](f func(A) K) func(as []A) []A {
|
||||
func Uniq[A any, K comparable](f func(A) K) Operator[A, A] {
|
||||
return G.Uniq[[]A](f)
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ import (
|
||||
//
|
||||
//go:inline
|
||||
func ZipWith[FCT ~func(A, B) C, A, B, C any](fa []A, fb []B, f FCT) []C {
|
||||
return G.ZipWith[[]A, []B, []C, FCT](fa, fb, f)
|
||||
return G.ZipWith[[]A, []B, []C](fa, fb, f)
|
||||
}
|
||||
|
||||
// Zip takes two arrays and returns an array of corresponding pairs (tuples).
|
||||
@@ -79,5 +79,5 @@ func Zip[A, B any](fb []B) func([]A) []T.Tuple2[A, B] {
|
||||
//
|
||||
//go:inline
|
||||
func Unzip[A, B any](cs []T.Tuple2[A, B]) T.Tuple2[[]A, []B] {
|
||||
return G.Unzip[[]A, []B, []T.Tuple2[A, B]](cs)
|
||||
return G.Unzip[[]A, []B](cs)
|
||||
}
|
||||
|
||||
710
v2/assert/assert.go
Normal file
710
v2/assert/assert.go
Normal file
@@ -0,0 +1,710 @@
|
||||
// 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.
|
||||
//
|
||||
// # Data Last Principle
|
||||
//
|
||||
// This package follows the "data last" functional programming principle, where
|
||||
// the data being operated on comes as the last parameter in a chain of function
|
||||
// applications. This design enables several powerful functional programming patterns:
|
||||
//
|
||||
// 1. **Partial Application**: You can create reusable assertion functions by providing
|
||||
// configuration parameters first, leaving the data and testing context for later.
|
||||
//
|
||||
// 2. **Function Composition**: Assertions can be composed and combined before being
|
||||
// applied to actual data.
|
||||
//
|
||||
// 3. **Point-Free Style**: You can pass assertion functions around without immediately
|
||||
// providing the data they operate on.
|
||||
//
|
||||
// The general pattern is:
|
||||
//
|
||||
// assert.Function(config)(data)(testingContext)
|
||||
// ↑ ↑ ↑
|
||||
// expected actual *testing.T (always last)
|
||||
//
|
||||
// For single-parameter assertions:
|
||||
//
|
||||
// assert.Function(data)(testingContext)
|
||||
// ↑ ↑
|
||||
// actual *testing.T (always last)
|
||||
//
|
||||
// Examples of "data last" in action:
|
||||
//
|
||||
// // Multi-parameter: expected value → actual value → testing context
|
||||
// assert.Equal(42)(result)(t)
|
||||
// assert.ArrayContains(3)(numbers)(t)
|
||||
//
|
||||
// // Single-parameter: data → testing context
|
||||
// assert.NoError(err)(t)
|
||||
// assert.ArrayNotEmpty(arr)(t)
|
||||
//
|
||||
// // Partial application - create reusable assertions
|
||||
// isPositive := assert.That(func(n int) bool { return n > 0 })
|
||||
// // Later, apply to different values:
|
||||
// isPositive(42)(t) // Passes
|
||||
// isPositive(-5)(t) // Fails
|
||||
//
|
||||
// // Composition - combine assertions before applying data
|
||||
// validateUser := func(u User) assert.Reader {
|
||||
// return assert.AllOf([]assert.Reader{
|
||||
// assert.Equal("Alice")(u.Name),
|
||||
// assert.That(func(age int) bool { return age >= 18 })(u.Age),
|
||||
// })
|
||||
// }
|
||||
// validateUser(user)(t)
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// This function follows the "data last" principle - you provide the expected value first,
|
||||
// then the actual value, and finally the testing.T context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestNotEqual(t *testing.T) {
|
||||
// value := 42
|
||||
// assert.NotEqual(10)(value)(t) // Passes: 42 != 10
|
||||
// assert.NotEqual(42)(value)(t) // Fails: 42 == 42
|
||||
// }
|
||||
func NotEqual[T any](expected T) Kleisli[T] {
|
||||
return wrap1(assert.NotEqual, expected)
|
||||
}
|
||||
|
||||
// Equal tests if the expected and the actual values are equal.
|
||||
//
|
||||
// This is one of the most commonly used assertions. It follows the "data last" principle -
|
||||
// you provide the expected value first, then the actual value, and finally the testing.T context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestEqual(t *testing.T) {
|
||||
// result := 2 + 2
|
||||
// assert.Equal(4)(result)(t) // Passes
|
||||
//
|
||||
// name := "Alice"
|
||||
// assert.Equal("Alice")(name)(t) // Passes
|
||||
//
|
||||
// // Can be composed with other assertions
|
||||
// user := User{Name: "Bob", Age: 30}
|
||||
// assertions := assert.AllOf([]assert.Reader{
|
||||
// assert.Equal("Bob")(user.Name),
|
||||
// assert.Equal(30)(user.Age),
|
||||
// })
|
||||
// assertions(t)
|
||||
// }
|
||||
func Equal[T any](expected T) Kleisli[T] {
|
||||
return wrap1(assert.Equal, expected)
|
||||
}
|
||||
|
||||
// ArrayNotEmpty checks if an array is not empty.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestArrayNotEmpty(t *testing.T) {
|
||||
// numbers := []int{1, 2, 3}
|
||||
// assert.ArrayNotEmpty(numbers)(t) // Passes
|
||||
//
|
||||
// empty := []int{}
|
||||
// assert.ArrayNotEmpty(empty)(t) // Fails
|
||||
// }
|
||||
func ArrayNotEmpty[T any](arr []T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.NotEmpty(t, arr)
|
||||
}
|
||||
}
|
||||
|
||||
// RecordNotEmpty checks if a map is not empty.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestRecordNotEmpty(t *testing.T) {
|
||||
// config := map[string]int{"timeout": 30, "retries": 3}
|
||||
// assert.RecordNotEmpty(config)(t) // Passes
|
||||
//
|
||||
// empty := map[string]int{}
|
||||
// assert.RecordNotEmpty(empty)(t) // Fails
|
||||
// }
|
||||
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.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestStringNotEmpty(t *testing.T) {
|
||||
// message := "Hello, World!"
|
||||
// assert.StringNotEmpty(message)(t) // Passes
|
||||
//
|
||||
// empty := ""
|
||||
// assert.StringNotEmpty(empty)(t) // Fails
|
||||
// }
|
||||
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.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestArrayLength(t *testing.T) {
|
||||
// numbers := []int{1, 2, 3, 4, 5}
|
||||
// assert.ArrayLength[int](5)(numbers)(t) // Passes
|
||||
// assert.ArrayLength[int](3)(numbers)(t) // Fails
|
||||
// }
|
||||
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.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestRecordLength(t *testing.T) {
|
||||
// config := map[string]string{"host": "localhost", "port": "8080"}
|
||||
// assert.RecordLength[string, string](2)(config)(t) // Passes
|
||||
// assert.RecordLength[string, string](3)(config)(t) // Fails
|
||||
// }
|
||||
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.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestStringLength(t *testing.T) {
|
||||
// message := "Hello"
|
||||
// assert.StringLength[any, any](5)(message)(t) // Passes
|
||||
// assert.StringLength[any, any](10)(message)(t) // Fails
|
||||
// }
|
||||
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.
|
||||
//
|
||||
// This is commonly used to assert that operations complete successfully.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestNoError(t *testing.T) {
|
||||
// err := doSomething()
|
||||
// assert.NoError(err)(t) // Passes if err is nil
|
||||
//
|
||||
// // Can be used with result types
|
||||
// result := result.TryCatch(func() (int, error) {
|
||||
// return 42, nil
|
||||
// })
|
||||
// assert.Success(result)(t) // Uses NoError internally
|
||||
// }
|
||||
func NoError(err error) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Error validates that there is an error.
|
||||
//
|
||||
// This is used to assert that operations fail as expected.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestError(t *testing.T) {
|
||||
// err := validateInput("")
|
||||
// assert.Error(err)(t) // Passes if err is not nil
|
||||
//
|
||||
// err2 := validateInput("valid")
|
||||
// assert.Error(err2)(t) // Fails if err2 is nil
|
||||
// }
|
||||
func Error(err error) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Success checks if a [Result] represents success.
|
||||
//
|
||||
// This is a convenience function for testing Result types from the fp-go library.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestSuccess(t *testing.T) {
|
||||
// res := result.Of[int](42)
|
||||
// assert.Success(res)(t) // Passes
|
||||
//
|
||||
// failedRes := result.Error[int](errors.New("failed"))
|
||||
// assert.Success(failedRes)(t) // Fails
|
||||
// }
|
||||
func Success[T any](res Result[T]) Reader {
|
||||
return NoError(result.ToError(res))
|
||||
}
|
||||
|
||||
// Failure checks if a [Result] represents failure.
|
||||
//
|
||||
// This is a convenience function for testing Result types from the fp-go library.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestFailure(t *testing.T) {
|
||||
// res := result.Error[int](errors.New("something went wrong"))
|
||||
// assert.Failure(res)(t) // Passes
|
||||
//
|
||||
// successRes := result.Of[int](42)
|
||||
// assert.Failure(successRes)(t) // Fails
|
||||
// }
|
||||
func Failure[T any](res Result[T]) Reader {
|
||||
return Error(result.ToError(res))
|
||||
}
|
||||
|
||||
// ArrayContains tests if a value is contained in an array.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestArrayContains(t *testing.T) {
|
||||
// numbers := []int{1, 2, 3, 4, 5}
|
||||
// assert.ArrayContains(3)(numbers)(t) // Passes
|
||||
// assert.ArrayContains(10)(numbers)(t) // Fails
|
||||
//
|
||||
// names := []string{"Alice", "Bob", "Charlie"}
|
||||
// assert.ArrayContains("Bob")(names)(t) // Passes
|
||||
// }
|
||||
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.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestContainsKey(t *testing.T) {
|
||||
// config := map[string]int{"timeout": 30, "retries": 3}
|
||||
// assert.ContainsKey[int]("timeout")(config)(t) // Passes
|
||||
// assert.ContainsKey[int]("maxSize")(config)(t) // Fails
|
||||
// }
|
||||
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.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestNotContainsKey(t *testing.T) {
|
||||
// config := map[string]int{"timeout": 30, "retries": 3}
|
||||
// assert.NotContainsKey[int]("maxSize")(config)(t) // Passes
|
||||
// assert.NotContainsKey[int]("timeout")(config)(t) // Fails
|
||||
// }
|
||||
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.
|
||||
//
|
||||
// This is a powerful function that allows you to create custom assertions using predicates.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestThat(t *testing.T) {
|
||||
// // Test if a number is positive
|
||||
// isPositive := func(n int) bool { return n > 0 }
|
||||
// assert.That(isPositive)(42)(t) // Passes
|
||||
// assert.That(isPositive)(-5)(t) // Fails
|
||||
//
|
||||
// // Test if a string is uppercase
|
||||
// isUppercase := func(s string) bool { return s == strings.ToUpper(s) }
|
||||
// assert.That(isUppercase)("HELLO")(t) // Passes
|
||||
// assert.That(isUppercase)("Hello")(t) // Fails
|
||||
//
|
||||
// // Can be combined with Local for property testing
|
||||
// type User struct { Age int }
|
||||
// ageIsAdult := assert.Local(func(u User) int { return u.Age })(
|
||||
// assert.That(func(age int) bool { return age >= 18 }),
|
||||
// )
|
||||
// user := User{Age: 25}
|
||||
// ageIsAdult(user)(t) // Passes
|
||||
// }
|
||||
func That[T any](pred Predicate[T]) Kleisli[T] {
|
||||
return func(a T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
if pred(a) {
|
||||
return true
|
||||
}
|
||||
return assert.Fail(t, fmt.Sprintf("Preficate %v does not match value %v", pred, a))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AllOf combines multiple assertion Readers into a single Reader that passes
|
||||
// only if all assertions pass.
|
||||
//
|
||||
// This function uses boolean AND logic (MonoidAll) to combine the results of
|
||||
// all assertions. If any assertion fails, the combined assertion fails.
|
||||
//
|
||||
// This is useful for grouping related assertions together and ensuring all
|
||||
// conditions are met.
|
||||
//
|
||||
// Parameters:
|
||||
// - readers: Array of assertion Readers to combine
|
||||
//
|
||||
// Returns:
|
||||
// - A single Reader that performs all assertions and returns true only if all pass
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestUser(t *testing.T) {
|
||||
// user := User{Name: "Alice", Age: 30, Active: true}
|
||||
// assertions := assert.AllOf([]assert.Reader{
|
||||
// assert.Equal("Alice")(user.Name),
|
||||
// assert.Equal(30)(user.Age),
|
||||
// assert.Equal(true)(user.Active),
|
||||
// })
|
||||
// assertions(t)
|
||||
// }
|
||||
//
|
||||
//go:inline
|
||||
func AllOf(readers []Reader) Reader {
|
||||
return reader.MonadReduceArrayM(readers, boolean.MonoidAll)
|
||||
}
|
||||
|
||||
// RunAll executes a map of named test cases, running each as a subtest.
|
||||
//
|
||||
// This function creates a Reader that runs multiple named test cases using
|
||||
// Go's t.Run for proper test isolation and reporting. Each test case is
|
||||
// executed as a separate subtest with its own name.
|
||||
//
|
||||
// The function returns true only if all subtests pass. This allows for
|
||||
// better test organization and clearer test output.
|
||||
//
|
||||
// Parameters:
|
||||
// - testcases: Map of test names to assertion Readers
|
||||
//
|
||||
// Returns:
|
||||
// - A Reader that executes all named test cases and returns true if all pass
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestMathOperations(t *testing.T) {
|
||||
// testcases := map[string]assert.Reader{
|
||||
// "addition": assert.Equal(4)(2 + 2),
|
||||
// "multiplication": assert.Equal(6)(2 * 3),
|
||||
// "subtraction": assert.Equal(1)(3 - 2),
|
||||
// }
|
||||
// assert.RunAll(testcases)(t)
|
||||
// }
|
||||
//
|
||||
//go:inline
|
||||
func RunAll(testcases map[string]Reader) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
current := true
|
||||
for k, r := range testcases {
|
||||
current = current && t.Run(k, func(t1 *testing.T) {
|
||||
r(t1)
|
||||
})
|
||||
}
|
||||
return current
|
||||
}
|
||||
}
|
||||
|
||||
// Local transforms a Reader that works on type R1 into a Reader that works on type R2,
|
||||
// by providing a function that converts R2 to R1. This allows you to focus a test on a
|
||||
// specific property or subset of a larger data structure.
|
||||
//
|
||||
// This is particularly useful when you have an assertion that operates on a specific field
|
||||
// or property, and you want to apply it to a complete object. Instead of extracting the
|
||||
// property and then asserting on it, you can transform the assertion to work directly
|
||||
// on the whole object.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that extracts or transforms R2 into R1
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms a Reader[R1, Reader] into a Reader[R2, Reader]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// // Create an assertion that checks if age is positive
|
||||
// ageIsPositive := assert.That(func(age int) bool { return age > 0 })
|
||||
//
|
||||
// // Focus this assertion on the Age field of User
|
||||
// userAgeIsPositive := assert.Local(func(u User) int { return u.Age })(ageIsPositive)
|
||||
//
|
||||
// // Now we can test the whole User object
|
||||
// user := User{Name: "Alice", Age: 30}
|
||||
// userAgeIsPositive(user)(t)
|
||||
//
|
||||
//go:inline
|
||||
func Local[R1, R2 any](f func(R2) R1) func(Kleisli[R1]) Kleisli[R2] {
|
||||
return reader.Local[Reader](f)
|
||||
}
|
||||
|
||||
// LocalL is similar to Local but uses a Lens to focus on a specific property.
|
||||
// A Lens is a functional programming construct that provides a composable way to
|
||||
// focus on a part of a data structure.
|
||||
//
|
||||
// This function is particularly useful when you want to focus a test on a specific
|
||||
// field of a struct using a lens, making the code more declarative and composable.
|
||||
// Lenses are often code-generated or predefined for common data structures.
|
||||
//
|
||||
// Parameters:
|
||||
// - l: A Lens that focuses from type S to type T
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms a Reader[T, Reader] into a Reader[S, Reader]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Email string
|
||||
// }
|
||||
//
|
||||
// // Assume we have a lens that focuses on the Email field
|
||||
// var emailLens = lens.Prop[Person, string]("Email")
|
||||
//
|
||||
// // Create an assertion for email format
|
||||
// validEmail := assert.That(func(email string) bool {
|
||||
// return strings.Contains(email, "@")
|
||||
// })
|
||||
//
|
||||
// // Focus this assertion on the Email property using a lens
|
||||
// validPersonEmail := assert.LocalL(emailLens)(validEmail)
|
||||
//
|
||||
// // Test a Person object
|
||||
// person := Person{Name: "Bob", Email: "bob@example.com"}
|
||||
// validPersonEmail(person)(t)
|
||||
//
|
||||
//go:inline
|
||||
func LocalL[S, T any](l Lens[S, T]) func(Kleisli[T]) Kleisli[S] {
|
||||
return reader.Local[Reader](l.Get)
|
||||
}
|
||||
|
||||
// fromOptionalGetter is an internal helper that creates an assertion Reader from
|
||||
// an optional getter function. It asserts that the optional value is present (Some).
|
||||
func fromOptionalGetter[S, T any](getter func(S) option.Option[T], msgAndArgs ...any) Kleisli[S] {
|
||||
return func(s S) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.True(t, option.IsSome(getter(s)), msgAndArgs...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FromOptional creates an assertion that checks if an Optional can successfully extract a value.
|
||||
// An Optional is an optic that represents an optional reference to a subpart of a data structure.
|
||||
//
|
||||
// This function is useful when you have an Optional optic and want to assert that the optional
|
||||
// value is present (Some) rather than absent (None). The assertion passes if the Optional's
|
||||
// GetOption returns Some, and fails if it returns None.
|
||||
//
|
||||
// This enables property-focused testing where you verify that a particular optional field or
|
||||
// sub-structure exists and is accessible.
|
||||
//
|
||||
// Parameters:
|
||||
// - opt: An Optional optic that focuses from type S to type T
|
||||
//
|
||||
// Returns:
|
||||
// - A Reader that asserts the optional value is present when applied to a value of type S
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Database *DatabaseConfig // Optional field
|
||||
// }
|
||||
//
|
||||
// type DatabaseConfig struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
//
|
||||
// // Create an Optional that focuses on the Database field
|
||||
// dbOptional := optional.MakeOptional(
|
||||
// func(c Config) option.Option[*DatabaseConfig] {
|
||||
// if c.Database != nil {
|
||||
// return option.Some(c.Database)
|
||||
// }
|
||||
// return option.None[*DatabaseConfig]()
|
||||
// },
|
||||
// func(c Config, db *DatabaseConfig) Config {
|
||||
// c.Database = db
|
||||
// return c
|
||||
// },
|
||||
// )
|
||||
//
|
||||
// // Assert that the database config is present
|
||||
// hasDatabaseConfig := assert.FromOptional(dbOptional)
|
||||
//
|
||||
// config := Config{Database: &DatabaseConfig{Host: "localhost", Port: 5432}}
|
||||
// hasDatabaseConfig(config)(t) // Passes
|
||||
//
|
||||
// emptyConfig := Config{Database: nil}
|
||||
// hasDatabaseConfig(emptyConfig)(t) // Fails
|
||||
//
|
||||
//go:inline
|
||||
func FromOptional[S, T any](opt Optional[S, T]) reader.Reader[S, Reader] {
|
||||
return fromOptionalGetter(opt.GetOption, "Optional: %s", opt)
|
||||
}
|
||||
|
||||
// FromPrism creates an assertion that checks if a Prism can successfully extract a value.
|
||||
// A Prism is an optic used to select part of a sum type (tagged union or variant).
|
||||
//
|
||||
// This function is useful when you have a Prism optic and want to assert that a value
|
||||
// matches a specific variant of a sum type. The assertion passes if the Prism's GetOption
|
||||
// returns Some (meaning the value is of the expected variant), and fails if it returns None
|
||||
// (meaning the value is a different variant).
|
||||
//
|
||||
// This enables variant-focused testing where you verify that a value is of a particular
|
||||
// type or matches a specific condition within a sum type.
|
||||
//
|
||||
// Parameters:
|
||||
// - p: A Prism optic that focuses from type S to type T
|
||||
//
|
||||
// Returns:
|
||||
// - A Reader that asserts the prism successfully extracts when applied to a value of type S
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Result interface{ isResult() }
|
||||
// type Success struct{ Value int }
|
||||
// type Failure struct{ Error string }
|
||||
//
|
||||
// func (Success) isResult() {}
|
||||
// func (Failure) isResult() {}
|
||||
//
|
||||
// // Create a Prism that focuses on Success variant
|
||||
// successPrism := prism.MakePrism(
|
||||
// func(r Result) option.Option[int] {
|
||||
// if s, ok := r.(Success); ok {
|
||||
// return option.Some(s.Value)
|
||||
// }
|
||||
// return option.None[int]()
|
||||
// },
|
||||
// func(v int) Result { return Success{Value: v} },
|
||||
// )
|
||||
//
|
||||
// // Assert that the result is a Success
|
||||
// isSuccess := assert.FromPrism(successPrism)
|
||||
//
|
||||
// result1 := Success{Value: 42}
|
||||
// isSuccess(result1)(t) // Passes
|
||||
//
|
||||
// result2 := Failure{Error: "something went wrong"}
|
||||
// isSuccess(result2)(t) // Fails
|
||||
//
|
||||
//go:inline
|
||||
func FromPrism[S, T any](p Prism[S, T]) reader.Reader[S, Reader] {
|
||||
return fromOptionalGetter(p.GetOption, "Prism: %s", p)
|
||||
}
|
||||
@@ -16,94 +16,676 @@
|
||||
package assert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
EQ "github.com/IBM/fp-go/v2/eq"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
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) func(actual T) E.Either[error, T] {
|
||||
return func(actual T) E.Either[error, T] {
|
||||
ok := wrapped(t, expected, actual)
|
||||
if ok {
|
||||
return E.Of[error](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 E.Left[T](errTest)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// NotEqual tests if the expected and the actual values are not equal
|
||||
func NotEqual[T any](t *testing.T, expected T) func(actual T) E.Either[error, 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) func(actual T) E.Either[error, 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) func(actual []T) E.Either[error, []T] {
|
||||
return func(actual []T) E.Either[error, []T] {
|
||||
ok := assert.Len(t, actual, expected)
|
||||
if ok {
|
||||
return E.Of[error](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 E.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) func(actual E.Either[error, T]) E.Either[error, T] {
|
||||
return func(actual E.Either[error, T]) E.Either[error, T] {
|
||||
return E.MonadFold(actual, func(e error) E.Either[error, T] {
|
||||
assert.NoError(t, e)
|
||||
return E.Left[T](e)
|
||||
}, func(value T) E.Either[error, T] {
|
||||
assert.NoError(t, nil)
|
||||
return E.Right[error](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) func(actual []T) E.Either[error, []T] {
|
||||
return func(actual []T) E.Either[error, []T] {
|
||||
ok := assert.Contains(t, actual, expected)
|
||||
if ok {
|
||||
return E.Of[error](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 E.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) func(actual map[K]T) E.Either[error, map[K]T] {
|
||||
return func(actual map[K]T) E.Either[error, map[K]T] {
|
||||
ok := assert.Contains(t, actual, expected)
|
||||
if ok {
|
||||
return E.Of[error](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 E.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) func(actual map[K]T) E.Either[error, map[K]T] {
|
||||
return func(actual map[K]T) E.Either[error, map[K]T] {
|
||||
ok := assert.NotContains(t, actual, expected)
|
||||
if ok {
|
||||
return E.Of[error](actual)
|
||||
}
|
||||
return E.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")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
235
v2/assert/example_test.go
Normal file
235
v2/assert/example_test.go
Normal file
@@ -0,0 +1,235 @@
|
||||
// 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_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/assert"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// Example_basicAssertions demonstrates basic equality and inequality assertions
|
||||
func Example_basicAssertions() {
|
||||
// This would be in a real test function
|
||||
var t *testing.T // placeholder for example
|
||||
|
||||
// Basic equality
|
||||
value := 42
|
||||
assert.Equal(42)(value)(t)
|
||||
|
||||
// String equality
|
||||
name := "Alice"
|
||||
assert.Equal("Alice")(name)(t)
|
||||
|
||||
// Inequality
|
||||
assert.NotEqual(10)(value)(t)
|
||||
}
|
||||
|
||||
// Example_arrayAssertions demonstrates array-related assertions
|
||||
func Example_arrayAssertions() {
|
||||
var t *testing.T // placeholder for example
|
||||
|
||||
numbers := []int{1, 2, 3, 4, 5}
|
||||
|
||||
// Check array is not empty
|
||||
assert.ArrayNotEmpty(numbers)(t)
|
||||
|
||||
// Check array length
|
||||
assert.ArrayLength[int](5)(numbers)(t)
|
||||
|
||||
// Check array contains a value
|
||||
assert.ArrayContains(3)(numbers)(t)
|
||||
}
|
||||
|
||||
// Example_mapAssertions demonstrates map-related assertions
|
||||
func Example_mapAssertions() {
|
||||
var t *testing.T // placeholder for example
|
||||
|
||||
config := map[string]int{
|
||||
"timeout": 30,
|
||||
"retries": 3,
|
||||
"maxSize": 1000,
|
||||
}
|
||||
|
||||
// Check map is not empty
|
||||
assert.RecordNotEmpty(config)(t)
|
||||
|
||||
// Check map length
|
||||
assert.RecordLength[string, int](3)(config)(t)
|
||||
|
||||
// Check map contains key
|
||||
assert.ContainsKey[int]("timeout")(config)(t)
|
||||
|
||||
// Check map does not contain key
|
||||
assert.NotContainsKey[int]("unknown")(config)(t)
|
||||
}
|
||||
|
||||
// Example_errorAssertions demonstrates error-related assertions
|
||||
func Example_errorAssertions() {
|
||||
var t *testing.T // placeholder for example
|
||||
|
||||
// Assert no error
|
||||
err := doSomethingSuccessful()
|
||||
assert.NoError(err)(t)
|
||||
|
||||
// Assert error exists
|
||||
err2 := doSomethingThatFails()
|
||||
assert.Error(err2)(t)
|
||||
}
|
||||
|
||||
// Example_resultAssertions demonstrates Result type assertions
|
||||
func Example_resultAssertions() {
|
||||
var t *testing.T // placeholder for example
|
||||
|
||||
// Assert success
|
||||
successResult := result.Of[int](42)
|
||||
assert.Success(successResult)(t)
|
||||
|
||||
// Assert failure
|
||||
failureResult := result.Left[int](errors.New("something went wrong"))
|
||||
assert.Failure(failureResult)(t)
|
||||
}
|
||||
|
||||
// Example_predicateAssertions demonstrates custom predicate assertions
|
||||
func Example_predicateAssertions() {
|
||||
var t *testing.T // placeholder for example
|
||||
|
||||
// Test if a number is positive
|
||||
isPositive := func(n int) bool { return n > 0 }
|
||||
assert.That(isPositive)(42)(t)
|
||||
|
||||
// Test if a string is uppercase
|
||||
isUppercase := func(s string) bool { return s == strings.ToUpper(s) }
|
||||
assert.That(isUppercase)("HELLO")(t)
|
||||
|
||||
// Test if a number is even
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
assert.That(isEven)(10)(t)
|
||||
}
|
||||
|
||||
// Example_allOf demonstrates combining multiple assertions
|
||||
func Example_allOf() {
|
||||
var t *testing.T // placeholder for example
|
||||
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Active bool
|
||||
}
|
||||
|
||||
user := User{Name: "Alice", Age: 30, Active: true}
|
||||
|
||||
// Combine multiple assertions
|
||||
assertions := assert.AllOf([]assert.Reader{
|
||||
assert.Equal("Alice")(user.Name),
|
||||
assert.Equal(30)(user.Age),
|
||||
assert.Equal(true)(user.Active),
|
||||
})
|
||||
|
||||
assertions(t)
|
||||
}
|
||||
|
||||
// Example_runAll demonstrates running named test cases
|
||||
func Example_runAll() {
|
||||
var t *testing.T // placeholder for example
|
||||
|
||||
testcases := map[string]assert.Reader{
|
||||
"addition": assert.Equal(4)(2 + 2),
|
||||
"multiplication": assert.Equal(6)(2 * 3),
|
||||
"subtraction": assert.Equal(1)(3 - 2),
|
||||
"division": assert.Equal(2)(10 / 5),
|
||||
}
|
||||
|
||||
assert.RunAll(testcases)(t)
|
||||
}
|
||||
|
||||
// Example_local demonstrates focusing assertions on specific properties
|
||||
func Example_local() {
|
||||
var t *testing.T // placeholder for 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)
|
||||
}
|
||||
|
||||
// Example_composableAssertions demonstrates building complex assertions
|
||||
func Example_composableAssertions() {
|
||||
var t *testing.T // placeholder for example
|
||||
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
Timeout int
|
||||
Retries int
|
||||
}
|
||||
|
||||
config := Config{
|
||||
Host: "localhost",
|
||||
Port: 8080,
|
||||
Timeout: 30,
|
||||
Retries: 3,
|
||||
}
|
||||
|
||||
// Create focused assertions for each field
|
||||
validHost := assert.Local(func(c Config) string { return c.Host })(
|
||||
assert.StringNotEmpty,
|
||||
)
|
||||
|
||||
validPort := assert.Local(func(c Config) int { return c.Port })(
|
||||
assert.That(func(p int) bool { return p > 0 && p < 65536 }),
|
||||
)
|
||||
|
||||
validTimeout := assert.Local(func(c Config) int { return c.Timeout })(
|
||||
assert.That(func(t int) bool { return t > 0 }),
|
||||
)
|
||||
|
||||
validRetries := assert.Local(func(c Config) int { return c.Retries })(
|
||||
assert.That(func(r int) bool { return r >= 0 }),
|
||||
)
|
||||
|
||||
// Combine all assertions
|
||||
validConfig := assert.AllOf([]assert.Reader{
|
||||
validHost(config),
|
||||
validPort(config),
|
||||
validTimeout(config),
|
||||
validRetries(config),
|
||||
})
|
||||
|
||||
validConfig(t)
|
||||
}
|
||||
|
||||
// Helper functions for examples
|
||||
func doSomethingSuccessful() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func doSomethingThatFails() error {
|
||||
return errors.New("operation failed")
|
||||
}
|
||||
22
v2/assert/types.go
Normal file
22
v2/assert/types.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package assert
|
||||
|
||||
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]
|
||||
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]
|
||||
)
|
||||
@@ -53,7 +53,7 @@ func MakeBounded[T any](o ord.Ord[T], t, b T) Bounded[T] {
|
||||
|
||||
// Clamp returns a function that clamps against the bounds defined in the bounded type
|
||||
func Clamp[T any](b Bounded[T]) func(T) T {
|
||||
return ord.Clamp[T](b)(b.Bottom(), b.Top())
|
||||
return ord.Clamp(b)(b.Bottom(), b.Top())
|
||||
}
|
||||
|
||||
// Reverse reverses the ordering and swaps the bounds
|
||||
|
||||
7
v2/builder/builder.go
Normal file
7
v2/builder/builder.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package builder
|
||||
|
||||
type (
|
||||
Builder[T any] interface {
|
||||
Build() Result[T]
|
||||
}
|
||||
)
|
||||
12
v2/builder/prism.go
Normal file
12
v2/builder/prism.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// BuilderPrism createa a [Prism] that converts between a builder and its type
|
||||
func BuilderPrism[T any, B Builder[T]](creator func(T) B) Prism[B, T] {
|
||||
return prism.MakePrismWithName(F.Flow2(B.Build, result.ToOption[T]), creator, "BuilderPrism")
|
||||
}
|
||||
15
v2/builder/types.go
Normal file
15
v2/builder/types.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
type (
|
||||
Result[T any] = result.Result[T]
|
||||
|
||||
Prism[S, A any] = prism.Prism[S, A]
|
||||
|
||||
Option[T any] = option.Option[T]
|
||||
)
|
||||
@@ -15,14 +15,163 @@
|
||||
|
||||
package bytes
|
||||
|
||||
// Empty returns an empty byte slice.
|
||||
//
|
||||
// This function returns the identity element for the byte slice Monoid,
|
||||
// which is an empty byte slice. It's useful as a starting point for
|
||||
// building byte slices or as a default value.
|
||||
//
|
||||
// Returns:
|
||||
// - An empty byte slice ([]byte{})
|
||||
//
|
||||
// Properties:
|
||||
// - Empty() is the identity element for Monoid.Concat
|
||||
// - Monoid.Concat(Empty(), x) == x
|
||||
// - Monoid.Concat(x, Empty()) == x
|
||||
//
|
||||
// Example - Basic usage:
|
||||
//
|
||||
// empty := Empty()
|
||||
// fmt.Println(len(empty)) // 0
|
||||
//
|
||||
// Example - As identity element:
|
||||
//
|
||||
// data := []byte("hello")
|
||||
// result1 := Monoid.Concat(Empty(), data) // []byte("hello")
|
||||
// result2 := Monoid.Concat(data, Empty()) // []byte("hello")
|
||||
//
|
||||
// Example - Building byte slices:
|
||||
//
|
||||
// // Start with empty and build up
|
||||
// buffer := Empty()
|
||||
// buffer = Monoid.Concat(buffer, []byte("Hello"))
|
||||
// buffer = Monoid.Concat(buffer, []byte(" "))
|
||||
// buffer = Monoid.Concat(buffer, []byte("World"))
|
||||
// // buffer: []byte("Hello World")
|
||||
//
|
||||
// See also:
|
||||
// - Monoid.Empty(): Alternative way to get empty byte slice
|
||||
// - ConcatAll(): For concatenating multiple byte slices
|
||||
func Empty() []byte {
|
||||
return Monoid.Empty()
|
||||
}
|
||||
|
||||
// ToString converts a byte slice to a string.
|
||||
//
|
||||
// This function performs a direct conversion from []byte to string.
|
||||
// The conversion creates a new string with a copy of the byte data.
|
||||
//
|
||||
// Parameters:
|
||||
// - a: The byte slice to convert
|
||||
//
|
||||
// Returns:
|
||||
// - A string containing the same data as the byte slice
|
||||
//
|
||||
// Performance Note:
|
||||
//
|
||||
// This conversion allocates a new string. For performance-critical code
|
||||
// that needs to avoid allocations, consider using unsafe.String (Go 1.20+)
|
||||
// or working directly with byte slices.
|
||||
//
|
||||
// Example - Basic conversion:
|
||||
//
|
||||
// bytes := []byte("hello")
|
||||
// str := ToString(bytes)
|
||||
// fmt.Println(str) // "hello"
|
||||
//
|
||||
// Example - Converting binary data:
|
||||
//
|
||||
// // ASCII codes for "Hello"
|
||||
// data := []byte{0x48, 0x65, 0x6c, 0x6c, 0x6f}
|
||||
// str := ToString(data)
|
||||
// fmt.Println(str) // "Hello"
|
||||
//
|
||||
// Example - Empty byte slice:
|
||||
//
|
||||
// empty := Empty()
|
||||
// str := ToString(empty)
|
||||
// fmt.Println(str == "") // true
|
||||
//
|
||||
// Example - UTF-8 encoded text:
|
||||
//
|
||||
// utf8Bytes := []byte("Hello, 世界")
|
||||
// str := ToString(utf8Bytes)
|
||||
// fmt.Println(str) // "Hello, 世界"
|
||||
//
|
||||
// Example - Round-trip conversion:
|
||||
//
|
||||
// original := "test string"
|
||||
// bytes := []byte(original)
|
||||
// result := ToString(bytes)
|
||||
// fmt.Println(original == result) // true
|
||||
//
|
||||
// See also:
|
||||
// - []byte(string): For converting string to byte slice
|
||||
// - Size(): For getting the length of a byte slice
|
||||
func ToString(a []byte) string {
|
||||
return string(a)
|
||||
}
|
||||
|
||||
// Size returns the number of bytes in a byte slice.
|
||||
//
|
||||
// This function returns the length of the byte slice, which is the number
|
||||
// of bytes it contains. This is equivalent to len(as) but provided as a
|
||||
// named function for use in functional composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The byte slice to measure
|
||||
//
|
||||
// Returns:
|
||||
// - The number of bytes in the slice
|
||||
//
|
||||
// Example - Basic usage:
|
||||
//
|
||||
// data := []byte("hello")
|
||||
// size := Size(data)
|
||||
// fmt.Println(size) // 5
|
||||
//
|
||||
// Example - Empty slice:
|
||||
//
|
||||
// empty := Empty()
|
||||
// size := Size(empty)
|
||||
// fmt.Println(size) // 0
|
||||
//
|
||||
// Example - Binary data:
|
||||
//
|
||||
// binary := []byte{0x01, 0x02, 0x03, 0x04}
|
||||
// size := Size(binary)
|
||||
// fmt.Println(size) // 4
|
||||
//
|
||||
// Example - UTF-8 encoded text:
|
||||
//
|
||||
// // Note: Size returns byte count, not character count
|
||||
// utf8 := []byte("Hello, 世界")
|
||||
// byteCount := Size(utf8)
|
||||
// fmt.Println(byteCount) // 13 (not 9 characters)
|
||||
//
|
||||
// Example - Using in functional composition:
|
||||
//
|
||||
// import "github.com/IBM/fp-go/v2/array"
|
||||
//
|
||||
// slices := [][]byte{
|
||||
// []byte("a"),
|
||||
// []byte("bb"),
|
||||
// []byte("ccc"),
|
||||
// }
|
||||
//
|
||||
// // Map to get sizes
|
||||
// sizes := array.Map(Size)(slices)
|
||||
// // sizes: []int{1, 2, 3}
|
||||
//
|
||||
// Example - Checking if slice is empty:
|
||||
//
|
||||
// data := []byte("test")
|
||||
// isEmpty := Size(data) == 0
|
||||
// fmt.Println(isEmpty) // false
|
||||
//
|
||||
// See also:
|
||||
// - len(): Built-in function for getting slice length
|
||||
// - ToString(): For converting byte slice to string
|
||||
func Size(as []byte) int {
|
||||
return len(as)
|
||||
}
|
||||
|
||||
@@ -187,6 +187,299 @@ func TestOrd(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestOrdProperties tests mathematical properties of Ord
|
||||
func TestOrdProperties(t *testing.T) {
|
||||
t.Run("reflexivity: x == x", func(t *testing.T) {
|
||||
testCases := [][]byte{
|
||||
[]byte{},
|
||||
[]byte("a"),
|
||||
[]byte("test"),
|
||||
[]byte{0x01, 0x02, 0x03},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
assert.Equal(t, 0, Ord.Compare(tc, tc),
|
||||
"Compare(%v, %v) should be 0", tc, tc)
|
||||
assert.True(t, Ord.Equals(tc, tc),
|
||||
"Equals(%v, %v) should be true", tc, tc)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("antisymmetry: if x <= y and y <= x then x == y", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
a, b []byte
|
||||
}{
|
||||
{[]byte("abc"), []byte("abc")},
|
||||
{[]byte{}, []byte{}},
|
||||
{[]byte{0x01}, []byte{0x01}},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
cmp1 := Ord.Compare(tc.a, tc.b)
|
||||
cmp2 := Ord.Compare(tc.b, tc.a)
|
||||
|
||||
if cmp1 <= 0 && cmp2 <= 0 {
|
||||
assert.True(t, Ord.Equals(tc.a, tc.b),
|
||||
"If %v <= %v and %v <= %v, they should be equal", tc.a, tc.b, tc.b, tc.a)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("transitivity: if x <= y and y <= z then x <= z", func(t *testing.T) {
|
||||
x := []byte("a")
|
||||
y := []byte("b")
|
||||
z := []byte("c")
|
||||
|
||||
cmpXY := Ord.Compare(x, y)
|
||||
cmpYZ := Ord.Compare(y, z)
|
||||
cmpXZ := Ord.Compare(x, z)
|
||||
|
||||
if cmpXY <= 0 && cmpYZ <= 0 {
|
||||
assert.True(t, cmpXZ <= 0,
|
||||
"If %v <= %v and %v <= %v, then %v <= %v", x, y, y, z, x, z)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("totality: either x <= y or y <= x", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
a, b []byte
|
||||
}{
|
||||
{[]byte("abc"), []byte("abd")},
|
||||
{[]byte("xyz"), []byte("abc")},
|
||||
{[]byte{}, []byte("a")},
|
||||
{[]byte{0x01}, []byte{0x02}},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
cmp1 := Ord.Compare(tc.a, tc.b)
|
||||
cmp2 := Ord.Compare(tc.b, tc.a)
|
||||
|
||||
assert.True(t, cmp1 <= 0 || cmp2 <= 0,
|
||||
"Either %v <= %v or %v <= %v must be true", tc.a, tc.b, tc.b, tc.a)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestEdgeCases tests edge cases and boundary conditions
|
||||
func TestEdgeCases(t *testing.T) {
|
||||
t.Run("very large byte slices", func(t *testing.T) {
|
||||
large := make([]byte, 1000000)
|
||||
for i := range large {
|
||||
large[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
size := Size(large)
|
||||
assert.Equal(t, 1000000, size)
|
||||
|
||||
str := ToString(large)
|
||||
assert.Equal(t, 1000000, len(str))
|
||||
})
|
||||
|
||||
t.Run("concatenating many slices", func(t *testing.T) {
|
||||
slices := make([][]byte, 100)
|
||||
for i := range slices {
|
||||
slices[i] = []byte{byte(i)}
|
||||
}
|
||||
|
||||
result := ConcatAll(slices...)
|
||||
assert.Equal(t, 100, Size(result))
|
||||
})
|
||||
|
||||
t.Run("null bytes in slice", func(t *testing.T) {
|
||||
data := []byte{0x00, 0x01, 0x00, 0x02}
|
||||
size := Size(data)
|
||||
assert.Equal(t, 4, size)
|
||||
|
||||
str := ToString(data)
|
||||
assert.Equal(t, 4, len(str))
|
||||
})
|
||||
|
||||
t.Run("comparing slices with null bytes", func(t *testing.T) {
|
||||
a := []byte{0x00, 0x01}
|
||||
b := []byte{0x00, 0x02}
|
||||
assert.Equal(t, -1, Ord.Compare(a, b))
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonoidConcatPerformance tests concatenation performance characteristics
|
||||
func TestMonoidConcatPerformance(t *testing.T) {
|
||||
t.Run("ConcatAll vs repeated Concat", func(t *testing.T) {
|
||||
slices := [][]byte{
|
||||
[]byte("a"),
|
||||
[]byte("b"),
|
||||
[]byte("c"),
|
||||
[]byte("d"),
|
||||
[]byte("e"),
|
||||
}
|
||||
|
||||
// Using ConcatAll
|
||||
result1 := ConcatAll(slices...)
|
||||
|
||||
// Using repeated Concat
|
||||
result2 := Monoid.Empty()
|
||||
for _, s := range slices {
|
||||
result2 = Monoid.Concat(result2, s)
|
||||
}
|
||||
|
||||
assert.Equal(t, result1, result2)
|
||||
assert.Equal(t, []byte("abcde"), result1)
|
||||
})
|
||||
}
|
||||
|
||||
// TestRoundTrip tests round-trip conversions
|
||||
func TestRoundTrip(t *testing.T) {
|
||||
t.Run("string to bytes to string", func(t *testing.T) {
|
||||
original := "Hello, World! 世界"
|
||||
bytes := []byte(original)
|
||||
result := ToString(bytes)
|
||||
assert.Equal(t, original, result)
|
||||
})
|
||||
|
||||
t.Run("bytes to string to bytes", func(t *testing.T) {
|
||||
original := []byte{0x48, 0x65, 0x6c, 0x6c, 0x6f}
|
||||
str := ToString(original)
|
||||
result := []byte(str)
|
||||
assert.Equal(t, original, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestConcatAllVariadic tests ConcatAll with various argument counts
|
||||
func TestConcatAllVariadic(t *testing.T) {
|
||||
t.Run("zero arguments", func(t *testing.T) {
|
||||
result := ConcatAll()
|
||||
assert.Equal(t, []byte{}, result)
|
||||
})
|
||||
|
||||
t.Run("one argument", func(t *testing.T) {
|
||||
result := ConcatAll([]byte("test"))
|
||||
assert.Equal(t, []byte("test"), result)
|
||||
})
|
||||
|
||||
t.Run("two arguments", func(t *testing.T) {
|
||||
result := ConcatAll([]byte("hello"), []byte("world"))
|
||||
assert.Equal(t, []byte("helloworld"), result)
|
||||
})
|
||||
|
||||
t.Run("many arguments", func(t *testing.T) {
|
||||
result := ConcatAll(
|
||||
[]byte("a"),
|
||||
[]byte("b"),
|
||||
[]byte("c"),
|
||||
[]byte("d"),
|
||||
[]byte("e"),
|
||||
[]byte("f"),
|
||||
[]byte("g"),
|
||||
[]byte("h"),
|
||||
[]byte("i"),
|
||||
[]byte("j"),
|
||||
)
|
||||
assert.Equal(t, []byte("abcdefghij"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkToString(b *testing.B) {
|
||||
data := []byte("Hello, World!")
|
||||
|
||||
b.Run("small", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
_ = ToString(data)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("large", func(b *testing.B) {
|
||||
large := make([]byte, 10000)
|
||||
for i := range large {
|
||||
large[i] = byte(i % 256)
|
||||
}
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = ToString(large)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkSize(b *testing.B) {
|
||||
data := []byte("Hello, World!")
|
||||
|
||||
for b.Loop() {
|
||||
_ = Size(data)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMonoidConcat(b *testing.B) {
|
||||
a := []byte("Hello")
|
||||
c := []byte(" World")
|
||||
|
||||
b.Run("small slices", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
_ = Monoid.Concat(a, c)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("large slices", func(b *testing.B) {
|
||||
large1 := make([]byte, 10000)
|
||||
large2 := make([]byte, 10000)
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = Monoid.Concat(large1, large2)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkConcatAll(b *testing.B) {
|
||||
slices := [][]byte{
|
||||
[]byte("Hello"),
|
||||
[]byte(" "),
|
||||
[]byte("World"),
|
||||
[]byte("!"),
|
||||
}
|
||||
|
||||
b.Run("few slices", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
_ = ConcatAll(slices...)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("many slices", func(b *testing.B) {
|
||||
many := make([][]byte, 100)
|
||||
for i := range many {
|
||||
many[i] = []byte{byte(i)}
|
||||
}
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = ConcatAll(many...)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkOrdCompare(b *testing.B) {
|
||||
a := []byte("abc")
|
||||
c := []byte("abd")
|
||||
|
||||
b.Run("equal", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
_ = Ord.Compare(a, a)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("different", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
_ = Ord.Compare(a, c)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("large slices", func(b *testing.B) {
|
||||
large1 := make([]byte, 10000)
|
||||
large2 := make([]byte, 10000)
|
||||
large2[9999] = 1
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = Ord.Compare(large1, large2)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Example tests
|
||||
func ExampleEmpty() {
|
||||
empty := Empty()
|
||||
@@ -219,3 +512,17 @@ func ExampleConcatAll() {
|
||||
|
||||
// Output:
|
||||
}
|
||||
|
||||
func ExampleMonoid_concat() {
|
||||
result := Monoid.Concat([]byte("Hello"), []byte(" World"))
|
||||
println(string(result)) // Hello World
|
||||
|
||||
// Output:
|
||||
}
|
||||
|
||||
func ExampleOrd_compare() {
|
||||
cmp := Ord.Compare([]byte("abc"), []byte("abd"))
|
||||
println(cmp) // -1 (abc < abd)
|
||||
|
||||
// Output:
|
||||
}
|
||||
|
||||
4
v2/bytes/coverage.out
Normal file
4
v2/bytes/coverage.out
Normal file
@@ -0,0 +1,4 @@
|
||||
mode: set
|
||||
github.com/IBM/fp-go/v2/bytes/bytes.go:55.21,57.2 1 1
|
||||
github.com/IBM/fp-go/v2/bytes/bytes.go:111.32,113.2 1 1
|
||||
github.com/IBM/fp-go/v2/bytes/bytes.go:175.26,177.2 1 1
|
||||
@@ -23,12 +23,219 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// monoid for byte arrays
|
||||
// Monoid is the Monoid instance for byte slices.
|
||||
//
|
||||
// This Monoid combines byte slices through concatenation, with an empty
|
||||
// byte slice as the identity element. It satisfies the monoid laws:
|
||||
//
|
||||
// Identity laws:
|
||||
// - Monoid.Concat(Monoid.Empty(), x) == x (left identity)
|
||||
// - Monoid.Concat(x, Monoid.Empty()) == x (right identity)
|
||||
//
|
||||
// Associativity law:
|
||||
// - Monoid.Concat(Monoid.Concat(a, b), c) == Monoid.Concat(a, Monoid.Concat(b, c))
|
||||
//
|
||||
// Operations:
|
||||
// - Empty(): Returns an empty byte slice []byte{}
|
||||
// - Concat(a, b []byte): Concatenates two byte slices
|
||||
//
|
||||
// Example - Basic concatenation:
|
||||
//
|
||||
// result := Monoid.Concat([]byte("Hello"), []byte(" World"))
|
||||
// // result: []byte("Hello World")
|
||||
//
|
||||
// Example - Identity element:
|
||||
//
|
||||
// empty := Monoid.Empty()
|
||||
// data := []byte("test")
|
||||
// result1 := Monoid.Concat(empty, data) // []byte("test")
|
||||
// result2 := Monoid.Concat(data, empty) // []byte("test")
|
||||
//
|
||||
// Example - Building byte buffers:
|
||||
//
|
||||
// buffer := Monoid.Empty()
|
||||
// buffer = Monoid.Concat(buffer, []byte("Line 1\n"))
|
||||
// buffer = Monoid.Concat(buffer, []byte("Line 2\n"))
|
||||
// buffer = Monoid.Concat(buffer, []byte("Line 3\n"))
|
||||
//
|
||||
// Example - Associativity:
|
||||
//
|
||||
// a := []byte("a")
|
||||
// b := []byte("b")
|
||||
// c := []byte("c")
|
||||
// left := Monoid.Concat(Monoid.Concat(a, b), c) // []byte("abc")
|
||||
// right := Monoid.Concat(a, Monoid.Concat(b, c)) // []byte("abc")
|
||||
// // left == right
|
||||
//
|
||||
// See also:
|
||||
// - ConcatAll: For concatenating multiple byte slices at once
|
||||
// - Empty(): Convenience function for getting empty byte slice
|
||||
Monoid = A.Monoid[byte]()
|
||||
|
||||
// ConcatAll concatenates all bytes
|
||||
// ConcatAll efficiently concatenates multiple byte slices into a single slice.
|
||||
//
|
||||
// This function takes a variadic number of byte slices and combines them
|
||||
// into a single byte slice. It pre-allocates the exact amount of memory
|
||||
// needed, making it more efficient than repeated concatenation.
|
||||
//
|
||||
// Parameters:
|
||||
// - slices: Zero or more byte slices to concatenate
|
||||
//
|
||||
// Returns:
|
||||
// - A new byte slice containing all input slices concatenated in order
|
||||
//
|
||||
// Performance:
|
||||
//
|
||||
// ConcatAll is more efficient than using Monoid.Concat repeatedly because
|
||||
// it calculates the total size upfront and allocates memory once, avoiding
|
||||
// multiple allocations and copies.
|
||||
//
|
||||
// Example - Basic usage:
|
||||
//
|
||||
// result := ConcatAll(
|
||||
// []byte("Hello"),
|
||||
// []byte(" "),
|
||||
// []byte("World"),
|
||||
// )
|
||||
// // result: []byte("Hello World")
|
||||
//
|
||||
// Example - Empty input:
|
||||
//
|
||||
// result := ConcatAll()
|
||||
// // result: []byte{}
|
||||
//
|
||||
// Example - Single slice:
|
||||
//
|
||||
// result := ConcatAll([]byte("test"))
|
||||
// // result: []byte("test")
|
||||
//
|
||||
// Example - Building protocol messages:
|
||||
//
|
||||
// import "encoding/binary"
|
||||
//
|
||||
// header := []byte{0x01, 0x02}
|
||||
// length := make([]byte, 4)
|
||||
// binary.BigEndian.PutUint32(length, 100)
|
||||
// payload := []byte("data")
|
||||
// footer := []byte{0xFF}
|
||||
//
|
||||
// message := ConcatAll(header, length, payload, footer)
|
||||
//
|
||||
// Example - With empty slices:
|
||||
//
|
||||
// result := ConcatAll(
|
||||
// []byte("a"),
|
||||
// []byte{},
|
||||
// []byte("b"),
|
||||
// []byte{},
|
||||
// []byte("c"),
|
||||
// )
|
||||
// // result: []byte("abc")
|
||||
//
|
||||
// Example - Building CSV line:
|
||||
//
|
||||
// fields := [][]byte{
|
||||
// []byte("John"),
|
||||
// []byte("Doe"),
|
||||
// []byte("30"),
|
||||
// }
|
||||
// separator := []byte(",")
|
||||
//
|
||||
// // Interleave fields with separators
|
||||
// parts := [][]byte{
|
||||
// fields[0], separator,
|
||||
// fields[1], separator,
|
||||
// fields[2],
|
||||
// }
|
||||
// line := ConcatAll(parts...)
|
||||
// // line: []byte("John,Doe,30")
|
||||
//
|
||||
// See also:
|
||||
// - Monoid.Concat: For concatenating exactly two byte slices
|
||||
// - bytes.Join: Standard library function for joining with separator
|
||||
ConcatAll = A.ArrayConcatAll[byte]
|
||||
|
||||
// Ord implements the default ordering on bytes
|
||||
// Ord is the Ord instance for byte slices providing lexicographic ordering.
|
||||
//
|
||||
// This Ord instance compares byte slices lexicographically (dictionary order),
|
||||
// comparing bytes from left to right until a difference is found or one slice
|
||||
// ends. It uses the standard library's bytes.Compare and bytes.Equal functions.
|
||||
//
|
||||
// Comparison rules:
|
||||
// - Compares byte-by-byte from left to right
|
||||
// - First differing byte determines the order
|
||||
// - Shorter slice is less than longer slice if all bytes match
|
||||
// - Empty slice is less than any non-empty slice
|
||||
//
|
||||
// Operations:
|
||||
// - Compare(a, b []byte) int: Returns -1 if a < b, 0 if a == b, 1 if a > b
|
||||
// - Equals(a, b []byte) bool: Returns true if slices are equal
|
||||
//
|
||||
// Example - Basic comparison:
|
||||
//
|
||||
// cmp := Ord.Compare([]byte("abc"), []byte("abd"))
|
||||
// // cmp: -1 (abc < abd)
|
||||
//
|
||||
// cmp = Ord.Compare([]byte("xyz"), []byte("abc"))
|
||||
// // cmp: 1 (xyz > abc)
|
||||
//
|
||||
// cmp = Ord.Compare([]byte("test"), []byte("test"))
|
||||
// // cmp: 0 (equal)
|
||||
//
|
||||
// Example - Length differences:
|
||||
//
|
||||
// cmp := Ord.Compare([]byte("ab"), []byte("abc"))
|
||||
// // cmp: -1 (shorter is less)
|
||||
//
|
||||
// cmp = Ord.Compare([]byte("abc"), []byte("ab"))
|
||||
// // cmp: 1 (longer is greater)
|
||||
//
|
||||
// Example - Empty slices:
|
||||
//
|
||||
// cmp := Ord.Compare([]byte{}, []byte("a"))
|
||||
// // cmp: -1 (empty is less)
|
||||
//
|
||||
// cmp = Ord.Compare([]byte{}, []byte{})
|
||||
// // cmp: 0 (both empty)
|
||||
//
|
||||
// Example - Equality check:
|
||||
//
|
||||
// equal := Ord.Equals([]byte("test"), []byte("test"))
|
||||
// // equal: true
|
||||
//
|
||||
// equal = Ord.Equals([]byte("test"), []byte("Test"))
|
||||
// // equal: false (case-sensitive)
|
||||
//
|
||||
// Example - Sorting byte slices:
|
||||
//
|
||||
// import "github.com/IBM/fp-go/v2/array"
|
||||
//
|
||||
// data := [][]byte{
|
||||
// []byte("zebra"),
|
||||
// []byte("apple"),
|
||||
// []byte("mango"),
|
||||
// }
|
||||
//
|
||||
// sorted := array.Sort(Ord)(data)
|
||||
// // sorted: [[]byte("apple"), []byte("mango"), []byte("zebra")]
|
||||
//
|
||||
// Example - Binary data comparison:
|
||||
//
|
||||
// cmp := Ord.Compare([]byte{0x01, 0x02}, []byte{0x01, 0x03})
|
||||
// // cmp: -1 (0x02 < 0x03)
|
||||
//
|
||||
// Example - Finding minimum:
|
||||
//
|
||||
// import O "github.com/IBM/fp-go/v2/ord"
|
||||
//
|
||||
// a := []byte("xyz")
|
||||
// b := []byte("abc")
|
||||
// min := O.Min(Ord)(a, b)
|
||||
// // min: []byte("abc")
|
||||
//
|
||||
// See also:
|
||||
// - bytes.Compare: Standard library comparison function
|
||||
// - bytes.Equal: Standard library equality function
|
||||
// - array.Sort: For sorting slices using an Ord instance
|
||||
Ord = O.MakeOrd(bytes.Compare, bytes.Equal)
|
||||
)
|
||||
|
||||
@@ -35,5 +35,6 @@ func Commands() []*C.Command {
|
||||
IOCommand(),
|
||||
IOOptionCommand(),
|
||||
DICommand(),
|
||||
LensCommand(),
|
||||
}
|
||||
}
|
||||
|
||||
839
v2/cli/lens.go
Normal file
839
v2/cli/lens.go
Normal file
@@ -0,0 +1,839 @@
|
||||
// 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 cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
keyLensDir = "dir"
|
||||
keyVerbose = "verbose"
|
||||
lensAnnotation = "fp-go:Lens"
|
||||
)
|
||||
|
||||
var (
|
||||
flagLensDir = &C.StringFlag{
|
||||
Name: keyLensDir,
|
||||
Value: ".",
|
||||
Usage: "Directory to scan for Go files",
|
||||
}
|
||||
|
||||
flagVerbose = &C.BoolFlag{
|
||||
Name: keyVerbose,
|
||||
Aliases: []string{"v"},
|
||||
Value: false,
|
||||
Usage: "Enable verbose output",
|
||||
}
|
||||
)
|
||||
|
||||
// structInfo holds information about a struct that needs lens generation
|
||||
type structInfo struct {
|
||||
Name string
|
||||
TypeParams string // e.g., "[T any]" or "[K comparable, V any]" - for type declarations
|
||||
TypeParamNames string // e.g., "[T]" or "[K, V]" - for type usage in function signatures
|
||||
Fields []fieldInfo
|
||||
Imports map[string]string // package path -> alias
|
||||
}
|
||||
|
||||
// fieldInfo holds information about a struct field
|
||||
type fieldInfo struct {
|
||||
Name string
|
||||
TypeName string
|
||||
BaseType string // TypeName without leading * for pointer types
|
||||
IsOptional bool // true if field is a pointer or has json omitempty tag
|
||||
IsComparable bool // true if the type is comparable (can use ==)
|
||||
}
|
||||
|
||||
// templateData holds data for template rendering
|
||||
type templateData struct {
|
||||
PackageName string
|
||||
Structs []structInfo
|
||||
}
|
||||
|
||||
const lensStructTemplate = `
|
||||
// {{.Name}}Lenses provides lenses for accessing fields of {{.Name}}
|
||||
type {{.Name}}Lenses{{.TypeParams}} struct {
|
||||
// mandatory fields
|
||||
{{- range .Fields}}
|
||||
{{.Name}} L.Lens[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{- end}}
|
||||
// optional fields
|
||||
{{- range .Fields}}
|
||||
{{- if .IsComparable}}
|
||||
{{.Name}}O LO.LensO[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
}
|
||||
|
||||
// {{.Name}}RefLenses provides lenses for accessing fields of {{.Name}} via a reference to {{.Name}}
|
||||
type {{.Name}}RefLenses{{.TypeParams}} struct {
|
||||
// mandatory fields
|
||||
{{- range .Fields}}
|
||||
{{.Name}} L.Lens[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{- end}}
|
||||
// optional fields
|
||||
{{- range .Fields}}
|
||||
{{- if .IsComparable}}
|
||||
{{.Name}}O LO.LensO[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
}
|
||||
`
|
||||
|
||||
const lensConstructorTemplate = `
|
||||
// Make{{.Name}}Lenses creates a new {{.Name}}Lenses with lenses for all fields
|
||||
func Make{{.Name}}Lenses{{.TypeParams}}() {{.Name}}Lenses{{.TypeParamNames}} {
|
||||
// mandatory lenses
|
||||
{{- range .Fields}}
|
||||
lens{{.Name}} := L.MakeLens(
|
||||
func(s {{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
|
||||
func(s {{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) {{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
|
||||
)
|
||||
{{- end}}
|
||||
// optional lenses
|
||||
{{- range .Fields}}
|
||||
{{- if .IsComparable}}
|
||||
lens{{.Name}}O := LO.FromIso[{{$.Name}}{{$.TypeParamNames}}](IO.FromZero[{{.TypeName}}]())(lens{{.Name}})
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
return {{.Name}}Lenses{{.TypeParamNames}}{
|
||||
// mandatory lenses
|
||||
{{- range .Fields}}
|
||||
{{.Name}}: lens{{.Name}},
|
||||
{{- end}}
|
||||
// optional lenses
|
||||
{{- range .Fields}}
|
||||
{{- if .IsComparable}}
|
||||
{{.Name}}O: lens{{.Name}}O,
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
}
|
||||
}
|
||||
|
||||
// Make{{.Name}}RefLenses creates a new {{.Name}}RefLenses with lenses for all fields
|
||||
func Make{{.Name}}RefLenses{{.TypeParams}}() {{.Name}}RefLenses{{.TypeParamNames}} {
|
||||
// mandatory lenses
|
||||
{{- range .Fields}}
|
||||
{{- if .IsComparable}}
|
||||
lens{{.Name}} := L.MakeLensStrict(
|
||||
func(s *{{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
|
||||
func(s *{{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
|
||||
)
|
||||
{{- else}}
|
||||
lens{{.Name}} := L.MakeLensRef(
|
||||
func(s *{{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
|
||||
func(s *{{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
|
||||
)
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
// optional lenses
|
||||
{{- range .Fields}}
|
||||
{{- if .IsComparable}}
|
||||
lens{{.Name}}O := LO.FromIso[*{{$.Name}}{{$.TypeParamNames}}](IO.FromZero[{{.TypeName}}]())(lens{{.Name}})
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
return {{.Name}}RefLenses{{.TypeParamNames}}{
|
||||
// mandatory lenses
|
||||
{{- range .Fields}}
|
||||
{{.Name}}: lens{{.Name}},
|
||||
{{- end}}
|
||||
// optional lenses
|
||||
{{- range .Fields}}
|
||||
{{- if .IsComparable}}
|
||||
{{.Name}}O: lens{{.Name}}O,
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
var (
|
||||
structTmpl *template.Template
|
||||
constructorTmpl *template.Template
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
structTmpl, err = template.New("struct").Parse(lensStructTemplate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
constructorTmpl, err = template.New("constructor").Parse(lensConstructorTemplate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// hasLensAnnotation checks if a comment group contains the lens annotation
|
||||
func hasLensAnnotation(doc *ast.CommentGroup) bool {
|
||||
if doc == nil {
|
||||
return false
|
||||
}
|
||||
for _, comment := range doc.List {
|
||||
if strings.Contains(comment.Text, lensAnnotation) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getTypeName extracts the type name from a field type expression
|
||||
func getTypeName(expr ast.Expr) string {
|
||||
switch t := expr.(type) {
|
||||
case *ast.Ident:
|
||||
return t.Name
|
||||
case *ast.StarExpr:
|
||||
return "*" + getTypeName(t.X)
|
||||
case *ast.ArrayType:
|
||||
return "[]" + getTypeName(t.Elt)
|
||||
case *ast.MapType:
|
||||
return "map[" + getTypeName(t.Key) + "]" + getTypeName(t.Value)
|
||||
case *ast.SelectorExpr:
|
||||
return getTypeName(t.X) + "." + t.Sel.Name
|
||||
case *ast.InterfaceType:
|
||||
return "interface{}"
|
||||
case *ast.IndexExpr:
|
||||
// Generic type with single type parameter (Go 1.18+)
|
||||
// e.g., Option[string]
|
||||
return getTypeName(t.X) + "[" + getTypeName(t.Index) + "]"
|
||||
case *ast.IndexListExpr:
|
||||
// Generic type with multiple type parameters (Go 1.18+)
|
||||
// e.g., Map[string, int]
|
||||
var params []string
|
||||
for _, index := range t.Indices {
|
||||
params = append(params, getTypeName(index))
|
||||
}
|
||||
return getTypeName(t.X) + "[" + strings.Join(params, ", ") + "]"
|
||||
default:
|
||||
return "any"
|
||||
}
|
||||
}
|
||||
|
||||
// extractImports extracts package imports from a type expression
|
||||
// Returns a map of package path -> package name
|
||||
func extractImports(expr ast.Expr, imports map[string]string) {
|
||||
switch t := expr.(type) {
|
||||
case *ast.StarExpr:
|
||||
extractImports(t.X, imports)
|
||||
case *ast.ArrayType:
|
||||
extractImports(t.Elt, imports)
|
||||
case *ast.MapType:
|
||||
extractImports(t.Key, imports)
|
||||
extractImports(t.Value, imports)
|
||||
case *ast.SelectorExpr:
|
||||
// This is a qualified identifier like "option.Option"
|
||||
if ident, ok := t.X.(*ast.Ident); ok {
|
||||
// ident.Name is the package name (e.g., "option")
|
||||
// We need to track this for import resolution
|
||||
imports[ident.Name] = ident.Name
|
||||
}
|
||||
case *ast.IndexExpr:
|
||||
// Generic type with single type parameter
|
||||
extractImports(t.X, imports)
|
||||
extractImports(t.Index, imports)
|
||||
case *ast.IndexListExpr:
|
||||
// Generic type with multiple type parameters
|
||||
extractImports(t.X, imports)
|
||||
for _, index := range t.Indices {
|
||||
extractImports(index, imports)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hasOmitEmpty checks if a struct tag contains json omitempty
|
||||
func hasOmitEmpty(tag *ast.BasicLit) bool {
|
||||
if tag == nil {
|
||||
return false
|
||||
}
|
||||
// Parse the struct tag
|
||||
tagValue := strings.Trim(tag.Value, "`")
|
||||
structTag := reflect.StructTag(tagValue)
|
||||
jsonTag := structTag.Get("json")
|
||||
|
||||
// Check if omitempty is present
|
||||
parts := strings.Split(jsonTag, ",")
|
||||
for _, part := range parts {
|
||||
if strings.TrimSpace(part) == "omitempty" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isPointerType checks if a type expression is a pointer
|
||||
func isPointerType(expr ast.Expr) bool {
|
||||
_, ok := expr.(*ast.StarExpr)
|
||||
return ok
|
||||
}
|
||||
|
||||
// isComparableType checks if a type expression represents a comparable type.
|
||||
// Comparable types in Go include:
|
||||
// - Basic types (bool, numeric types, string)
|
||||
// - Pointer types
|
||||
// - Channel types
|
||||
// - Interface types
|
||||
// - Structs where all fields are comparable
|
||||
// - Arrays where the element type is comparable
|
||||
//
|
||||
// Non-comparable types include:
|
||||
// - Slices
|
||||
// - Maps
|
||||
// - Functions
|
||||
//
|
||||
// typeParams is a map of type parameter names to their constraints (e.g., "T" -> "any", "K" -> "comparable")
|
||||
func isComparableType(expr ast.Expr, typeParams map[string]string) bool {
|
||||
switch t := expr.(type) {
|
||||
case *ast.Ident:
|
||||
// Check if this is a type parameter
|
||||
if constraint, isTypeParam := typeParams[t.Name]; isTypeParam {
|
||||
// Type parameter - check its constraint
|
||||
return constraint == "comparable"
|
||||
}
|
||||
|
||||
// Basic types and named types
|
||||
// We assume named types are comparable unless they're known non-comparable types
|
||||
name := t.Name
|
||||
// Known non-comparable built-in types
|
||||
if name == "error" {
|
||||
// error is an interface, which is comparable
|
||||
return true
|
||||
}
|
||||
// Most basic types and named types are comparable
|
||||
// We can't determine if a custom type is comparable without type checking,
|
||||
// so we assume it is (conservative approach)
|
||||
return true
|
||||
case *ast.StarExpr:
|
||||
// Pointer types are always comparable
|
||||
return true
|
||||
case *ast.ArrayType:
|
||||
// Arrays are comparable if their element type is comparable
|
||||
if t.Len == nil {
|
||||
// This is a slice (no length), slices are not comparable
|
||||
return false
|
||||
}
|
||||
// Fixed-size array, check element type
|
||||
return isComparableType(t.Elt, typeParams)
|
||||
case *ast.MapType:
|
||||
// Maps are not comparable
|
||||
return false
|
||||
case *ast.FuncType:
|
||||
// Functions are not comparable
|
||||
return false
|
||||
case *ast.InterfaceType:
|
||||
// Interface types are comparable
|
||||
return true
|
||||
case *ast.StructType:
|
||||
// Structs are comparable if all fields are comparable
|
||||
// We can't easily determine this without full type information,
|
||||
// so we conservatively return false for struct literals
|
||||
return false
|
||||
case *ast.SelectorExpr:
|
||||
// Qualified identifier (e.g., pkg.Type)
|
||||
// We can't determine comparability without type information
|
||||
// Check for known non-comparable types from standard library
|
||||
if ident, ok := t.X.(*ast.Ident); ok {
|
||||
pkgName := ident.Name
|
||||
typeName := t.Sel.Name
|
||||
// Check for known non-comparable types
|
||||
if pkgName == "context" && typeName == "Context" {
|
||||
// context.Context is an interface, which is comparable
|
||||
return true
|
||||
}
|
||||
// For other qualified types, we assume they're comparable
|
||||
// This is a conservative approach
|
||||
}
|
||||
return true
|
||||
case *ast.IndexExpr, *ast.IndexListExpr:
|
||||
// Generic types - we can't determine comparability without type information
|
||||
// For common generic types, we can make educated guesses
|
||||
var baseExpr ast.Expr
|
||||
if idx, ok := t.(*ast.IndexExpr); ok {
|
||||
baseExpr = idx.X
|
||||
} else if idxList, ok := t.(*ast.IndexListExpr); ok {
|
||||
baseExpr = idxList.X
|
||||
}
|
||||
|
||||
if sel, ok := baseExpr.(*ast.SelectorExpr); ok {
|
||||
if ident, ok := sel.X.(*ast.Ident); ok {
|
||||
pkgName := ident.Name
|
||||
typeName := sel.Sel.Name
|
||||
// Check for known non-comparable generic types
|
||||
if pkgName == "option" && typeName == "Option" {
|
||||
// Option types are not comparable (they contain a slice internally)
|
||||
return false
|
||||
}
|
||||
if pkgName == "either" && typeName == "Either" {
|
||||
// Either types are not comparable
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
// For other generic types, conservatively assume not comparable
|
||||
log.Printf("Not comparable type: %v\n", t)
|
||||
return false
|
||||
case *ast.ChanType:
|
||||
// Channel types are comparable
|
||||
return true
|
||||
default:
|
||||
// Unknown type, conservatively assume not comparable
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// embeddedFieldResult holds both the field info and its AST type for import extraction
|
||||
type embeddedFieldResult struct {
|
||||
fieldInfo fieldInfo
|
||||
fieldType ast.Expr
|
||||
}
|
||||
|
||||
// extractEmbeddedFields extracts fields from an embedded struct type
|
||||
// It returns a slice of embeddedFieldResult for all exported fields in the embedded struct
|
||||
// typeParamsMap contains the type parameters of the parent struct (for checking comparability)
|
||||
func extractEmbeddedFields(embedType ast.Expr, fileImports map[string]string, file *ast.File, typeParamsMap map[string]string) []embeddedFieldResult {
|
||||
var results []embeddedFieldResult
|
||||
|
||||
// Get the type name of the embedded field
|
||||
var typeName string
|
||||
var typeIdent *ast.Ident
|
||||
|
||||
switch t := embedType.(type) {
|
||||
case *ast.Ident:
|
||||
// Direct embedded type: type MyStruct struct { EmbeddedType }
|
||||
typeName = t.Name
|
||||
typeIdent = t
|
||||
case *ast.StarExpr:
|
||||
// Pointer embedded type: type MyStruct struct { *EmbeddedType }
|
||||
if ident, ok := t.X.(*ast.Ident); ok {
|
||||
typeName = ident.Name
|
||||
typeIdent = ident
|
||||
}
|
||||
case *ast.SelectorExpr:
|
||||
// Qualified embedded type: type MyStruct struct { pkg.EmbeddedType }
|
||||
// We can't easily resolve this without full type information
|
||||
// For now, skip these
|
||||
return results
|
||||
}
|
||||
|
||||
if typeName == "" || typeIdent == nil {
|
||||
return results
|
||||
}
|
||||
|
||||
// Find the struct definition in the same file
|
||||
var embeddedStructType *ast.StructType
|
||||
ast.Inspect(file, func(n ast.Node) bool {
|
||||
if ts, ok := n.(*ast.TypeSpec); ok {
|
||||
if ts.Name.Name == typeName {
|
||||
if st, ok := ts.Type.(*ast.StructType); ok {
|
||||
embeddedStructType = st
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if embeddedStructType == nil {
|
||||
// Struct not found in this file, might be from another package
|
||||
return results
|
||||
}
|
||||
|
||||
// Extract fields from the embedded struct
|
||||
for _, field := range embeddedStructType.Fields.List {
|
||||
// Skip embedded fields within embedded structs (for now, to avoid infinite recursion)
|
||||
if len(field.Names) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, name := range field.Names {
|
||||
// Only export lenses for exported fields
|
||||
if name.IsExported() {
|
||||
fieldTypeName := getTypeName(field.Type)
|
||||
isOptional := false
|
||||
baseType := fieldTypeName
|
||||
|
||||
// Check if field is optional
|
||||
if isPointerType(field.Type) {
|
||||
isOptional = true
|
||||
baseType = strings.TrimPrefix(fieldTypeName, "*")
|
||||
} else if hasOmitEmpty(field.Tag) {
|
||||
isOptional = true
|
||||
}
|
||||
|
||||
// Check if the type is comparable
|
||||
isComparable := isComparableType(field.Type, typeParamsMap)
|
||||
|
||||
results = append(results, embeddedFieldResult{
|
||||
fieldInfo: fieldInfo{
|
||||
Name: name.Name,
|
||||
TypeName: fieldTypeName,
|
||||
BaseType: baseType,
|
||||
IsOptional: isOptional,
|
||||
IsComparable: isComparable,
|
||||
},
|
||||
fieldType: field.Type,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// extractTypeParams extracts type parameters from a type spec
|
||||
// Returns two strings: full params like "[T any]" and names only like "[T]"
|
||||
func extractTypeParams(typeSpec *ast.TypeSpec) (string, string) {
|
||||
if typeSpec.TypeParams == nil || len(typeSpec.TypeParams.List) == 0 {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
var params []string
|
||||
var names []string
|
||||
for _, field := range typeSpec.TypeParams.List {
|
||||
for _, name := range field.Names {
|
||||
constraint := getTypeName(field.Type)
|
||||
params = append(params, name.Name+" "+constraint)
|
||||
names = append(names, name.Name)
|
||||
}
|
||||
}
|
||||
|
||||
fullParams := "[" + strings.Join(params, ", ") + "]"
|
||||
nameParams := "[" + strings.Join(names, ", ") + "]"
|
||||
return fullParams, nameParams
|
||||
}
|
||||
|
||||
// buildTypeParamsMap creates a map of type parameter names to their constraints
|
||||
// e.g., for "type Box[T any, K comparable]", returns {"T": "any", "K": "comparable"}
|
||||
func buildTypeParamsMap(typeSpec *ast.TypeSpec) map[string]string {
|
||||
typeParamsMap := make(map[string]string)
|
||||
if typeSpec.TypeParams == nil || len(typeSpec.TypeParams.List) == 0 {
|
||||
return typeParamsMap
|
||||
}
|
||||
|
||||
for _, field := range typeSpec.TypeParams.List {
|
||||
constraint := getTypeName(field.Type)
|
||||
for _, name := range field.Names {
|
||||
typeParamsMap[name.Name] = constraint
|
||||
}
|
||||
}
|
||||
|
||||
return typeParamsMap
|
||||
}
|
||||
|
||||
// parseFile parses a Go file and extracts structs with lens annotations
|
||||
func parseFile(filename string) ([]structInfo, string, error) {
|
||||
fset := token.NewFileSet()
|
||||
node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
var structs []structInfo
|
||||
packageName := node.Name.Name
|
||||
|
||||
// Build import map: package name -> import path
|
||||
fileImports := make(map[string]string)
|
||||
for _, imp := range node.Imports {
|
||||
path := strings.Trim(imp.Path.Value, `"`)
|
||||
var name string
|
||||
if imp.Name != nil {
|
||||
name = imp.Name.Name
|
||||
} else {
|
||||
// Extract package name from path (last component)
|
||||
parts := strings.Split(path, "/")
|
||||
name = parts[len(parts)-1]
|
||||
}
|
||||
fileImports[name] = path
|
||||
}
|
||||
|
||||
// First pass: collect all GenDecls with their doc comments
|
||||
declMap := make(map[*ast.TypeSpec]*ast.CommentGroup)
|
||||
ast.Inspect(node, func(n ast.Node) bool {
|
||||
if gd, ok := n.(*ast.GenDecl); ok {
|
||||
for _, spec := range gd.Specs {
|
||||
if ts, ok := spec.(*ast.TypeSpec); ok {
|
||||
declMap[ts] = gd.Doc
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
// Second pass: process type specs
|
||||
ast.Inspect(node, func(n ast.Node) bool {
|
||||
// Look for type declarations
|
||||
typeSpec, ok := n.(*ast.TypeSpec)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if it's a struct type
|
||||
structType, ok := typeSpec.Type.(*ast.StructType)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// Get the doc comment from our map
|
||||
doc := declMap[typeSpec]
|
||||
if !hasLensAnnotation(doc) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Extract field information and collect imports
|
||||
var fields []fieldInfo
|
||||
structImports := make(map[string]string)
|
||||
|
||||
// Build type parameters map for this struct
|
||||
typeParamsMap := buildTypeParamsMap(typeSpec)
|
||||
|
||||
for _, field := range structType.Fields.List {
|
||||
if len(field.Names) == 0 {
|
||||
// Embedded field - promote its fields
|
||||
embeddedResults := extractEmbeddedFields(field.Type, fileImports, node, typeParamsMap)
|
||||
for _, embResult := range embeddedResults {
|
||||
// Extract imports from embedded field's type
|
||||
fieldImports := make(map[string]string)
|
||||
extractImports(embResult.fieldType, fieldImports)
|
||||
|
||||
// Resolve package names to full import paths
|
||||
for pkgName := range fieldImports {
|
||||
if importPath, ok := fileImports[pkgName]; ok {
|
||||
structImports[importPath] = pkgName
|
||||
}
|
||||
}
|
||||
|
||||
fields = append(fields, embResult.fieldInfo)
|
||||
}
|
||||
continue
|
||||
}
|
||||
for _, name := range field.Names {
|
||||
// Only export lenses for exported fields
|
||||
if name.IsExported() {
|
||||
typeName := getTypeName(field.Type)
|
||||
isOptional := false
|
||||
baseType := typeName
|
||||
isComparable := false
|
||||
|
||||
// Check if field is optional:
|
||||
// 1. Pointer types are always optional
|
||||
// 2. Non-pointer types with json omitempty tag are optional
|
||||
if isPointerType(field.Type) {
|
||||
isOptional = true
|
||||
// Strip leading * for base type
|
||||
baseType = strings.TrimPrefix(typeName, "*")
|
||||
} else if hasOmitEmpty(field.Tag) {
|
||||
// Non-pointer type with omitempty is also optional
|
||||
isOptional = true
|
||||
}
|
||||
|
||||
// Check if the type is comparable (for non-optional fields)
|
||||
// For optional fields, we don't need to check since they use LensO
|
||||
isComparable = isComparableType(field.Type, typeParamsMap)
|
||||
// log.Printf("field %s, type: %v, isComparable: %b\n", name, field.Type, isComparable)
|
||||
|
||||
// Extract imports from this field's type
|
||||
fieldImports := make(map[string]string)
|
||||
extractImports(field.Type, fieldImports)
|
||||
|
||||
// Resolve package names to full import paths
|
||||
for pkgName := range fieldImports {
|
||||
if importPath, ok := fileImports[pkgName]; ok {
|
||||
structImports[importPath] = pkgName
|
||||
}
|
||||
}
|
||||
|
||||
fields = append(fields, fieldInfo{
|
||||
Name: name.Name,
|
||||
TypeName: typeName,
|
||||
BaseType: baseType,
|
||||
IsOptional: isOptional,
|
||||
IsComparable: isComparable,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(fields) > 0 {
|
||||
typeParams, typeParamNames := extractTypeParams(typeSpec)
|
||||
structs = append(structs, structInfo{
|
||||
Name: typeSpec.Name.Name,
|
||||
TypeParams: typeParams,
|
||||
TypeParamNames: typeParamNames,
|
||||
Fields: fields,
|
||||
Imports: structImports,
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
return structs, packageName, nil
|
||||
}
|
||||
|
||||
// generateLensHelpers scans a directory for Go files and generates lens code
|
||||
func generateLensHelpers(dir, filename string, verbose bool) error {
|
||||
// Get absolute path
|
||||
absDir, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if verbose {
|
||||
log.Printf("Scanning directory: %s", absDir)
|
||||
}
|
||||
|
||||
// Find all Go files in the directory
|
||||
files, err := filepath.Glob(filepath.Join(absDir, "*.go"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if verbose {
|
||||
log.Printf("Found %d Go files", len(files))
|
||||
}
|
||||
|
||||
// Parse all files and collect structs
|
||||
var allStructs []structInfo
|
||||
var packageName string
|
||||
|
||||
for _, file := range files {
|
||||
// Skip generated files and test files
|
||||
if strings.HasSuffix(file, "_test.go") || strings.Contains(file, "gen.go") {
|
||||
if verbose {
|
||||
log.Printf("Skipping file: %s", filepath.Base(file))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if verbose {
|
||||
log.Printf("Parsing file: %s", filepath.Base(file))
|
||||
}
|
||||
|
||||
structs, pkg, err := parseFile(file)
|
||||
if err != nil {
|
||||
log.Printf("Warning: failed to parse %s: %v", file, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if verbose && len(structs) > 0 {
|
||||
log.Printf("Found %d annotated struct(s) in %s", len(structs), filepath.Base(file))
|
||||
for _, s := range structs {
|
||||
log.Printf(" - %s (%d fields)", s.Name, len(s.Fields))
|
||||
}
|
||||
}
|
||||
|
||||
if packageName == "" {
|
||||
packageName = pkg
|
||||
}
|
||||
|
||||
allStructs = append(allStructs, structs...)
|
||||
}
|
||||
|
||||
if len(allStructs) == 0 {
|
||||
log.Printf("No structs with %s annotation found in %s", lensAnnotation, absDir)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Collect all unique imports from all structs
|
||||
allImports := make(map[string]string) // import path -> alias
|
||||
for _, s := range allStructs {
|
||||
for importPath, alias := range s.Imports {
|
||||
allImports[importPath] = alias
|
||||
}
|
||||
}
|
||||
|
||||
// Create output file
|
||||
outPath := filepath.Join(absDir, filename)
|
||||
f, err := os.Create(filepath.Clean(outPath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
log.Printf("Generating lens code in [%s] for package [%s] with [%d] structs ...", outPath, packageName, len(allStructs))
|
||||
|
||||
// Write header
|
||||
writePackage(f, packageName)
|
||||
|
||||
// Write imports
|
||||
f.WriteString("import (\n")
|
||||
// Standard fp-go imports always needed
|
||||
f.WriteString("\tL \"github.com/IBM/fp-go/v2/optics/lens\"\n")
|
||||
f.WriteString("\tLO \"github.com/IBM/fp-go/v2/optics/lens/option\"\n")
|
||||
// f.WriteString("\tO \"github.com/IBM/fp-go/v2/option\"\n")
|
||||
f.WriteString("\tIO \"github.com/IBM/fp-go/v2/optics/iso/option\"\n")
|
||||
|
||||
// Add additional imports collected from field types
|
||||
for importPath, alias := range allImports {
|
||||
f.WriteString("\t" + alias + " \"" + importPath + "\"\n")
|
||||
}
|
||||
|
||||
f.WriteString(")\n")
|
||||
|
||||
// Generate lens code for each struct using templates
|
||||
for _, s := range allStructs {
|
||||
var buf bytes.Buffer
|
||||
|
||||
// Generate struct type
|
||||
if err := structTmpl.Execute(&buf, s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Generate constructor
|
||||
if err := constructorTmpl.Execute(&buf, s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write to file
|
||||
if _, err := f.Write(buf.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LensCommand creates the CLI command for lens generation
|
||||
func LensCommand() *C.Command {
|
||||
return &C.Command{
|
||||
Name: "lens",
|
||||
Usage: "generate lens code for annotated structs",
|
||||
Description: "Scans Go files for structs annotated with 'fp-go:Lens' and generates lens types. Pointer types and non-pointer types with json omitempty tag generate LensO (optional lens).",
|
||||
Flags: []C.Flag{
|
||||
flagLensDir,
|
||||
flagFilename,
|
||||
flagVerbose,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
return generateLensHelpers(
|
||||
ctx.String(keyLensDir),
|
||||
ctx.String(keyFilename),
|
||||
ctx.Bool(keyVerbose),
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
1084
v2/cli/lens_test.go
Normal file
1084
v2/cli/lens_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -27,7 +27,7 @@ import (
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
fa := Make[string, int]("foo")
|
||||
assert.Equal(t, fa, F.Pipe1(fa, Map[string, int](utils.Double)))
|
||||
assert.Equal(t, fa, F.Pipe1(fa, Map[string](utils.Double)))
|
||||
}
|
||||
|
||||
func TestOf(t *testing.T) {
|
||||
|
||||
11
v2/constant/monoid.go
Normal file
11
v2/constant/monoid.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package constant
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
)
|
||||
|
||||
// Monoid returns a [M.Monoid] that returns a constant value in all operations
|
||||
func Monoid[A any](a A) M.Monoid[A] {
|
||||
return M.MakeMonoid(function.Constant2[A, A](a), a)
|
||||
}
|
||||
@@ -13,20 +13,19 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ioeither
|
||||
package ioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
IOE "github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// withContext wraps an existing IOEither and performs a context check for cancellation before delegating
|
||||
func WithContext[A any](ctx context.Context, ma IOE.IOEither[error, A]) IOE.IOEither[error, A] {
|
||||
return func() either.Either[error, A] {
|
||||
func WithContext[A any](ctx context.Context, ma IOResult[A]) IOResult[A] {
|
||||
return func() Result[A] {
|
||||
if err := context.Cause(ctx); err != nil {
|
||||
return either.Left[A](err)
|
||||
return result.Left[A](err)
|
||||
}
|
||||
return ma()
|
||||
}
|
||||
11
v2/context/ioresult/types.go
Normal file
11
v2/context/ioresult/types.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package ioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
type (
|
||||
IOResult[T any] = ioresult.IOResult[T]
|
||||
Result[T any] = result.Result[T]
|
||||
)
|
||||
@@ -1,68 +0,0 @@
|
||||
// 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 readereither
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
G "github.com/IBM/fp-go/v2/readereither/generic"
|
||||
)
|
||||
|
||||
// Bind creates an empty context of type [S] to be used with the [Bind] operation
|
||||
func Do[S any](
|
||||
empty S,
|
||||
) ReaderEither[S] {
|
||||
return G.Do[ReaderEither[S], context.Context, error, S](empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) ReaderEither[T],
|
||||
) func(ReaderEither[S1]) ReaderEither[S2] {
|
||||
return G.Bind[ReaderEither[S1], ReaderEither[S2], ReaderEither[T], context.Context, error, S1, S2, T](setter, f)
|
||||
}
|
||||
|
||||
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
func Let[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
) func(ReaderEither[S1]) ReaderEither[S2] {
|
||||
return G.Let[ReaderEither[S1], ReaderEither[S2], context.Context, error, S1, S2, T](setter, f)
|
||||
}
|
||||
|
||||
// LetTo attaches the a value to a context [S1] to produce a context [S2]
|
||||
func LetTo[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
) func(ReaderEither[S1]) ReaderEither[S2] {
|
||||
return G.LetTo[ReaderEither[S1], ReaderEither[S2], context.Context, error, S1, S2, T](setter, b)
|
||||
}
|
||||
|
||||
// BindTo initializes a new state [S1] from a value [T]
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
) func(ReaderEither[T]) ReaderEither[S1] {
|
||||
return G.BindTo[ReaderEither[S1], ReaderEither[T], context.Context, error, S1, T](setter)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
|
||||
func ApS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa ReaderEither[T],
|
||||
) func(ReaderEither[S1]) ReaderEither[S2] {
|
||||
return G.ApS[ReaderEither[S1], ReaderEither[S2], ReaderEither[T], context.Context, error, S1, S2, T](setter, fa)
|
||||
}
|
||||
20
v2/context/readerio/flip.go
Normal file
20
v2/context/readerio/flip.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package readerio
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
RIO "github.com/IBM/fp-go/v2/readerio"
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func SequenceReader[R, A any](ma ReaderIO[Reader[R, A]]) Reader[R, ReaderIO[A]] {
|
||||
return RIO.SequenceReader(ma)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TraverseReader[R, A, B any](
|
||||
f reader.Kleisli[R, A, B],
|
||||
) func(ReaderIO[A]) Kleisli[R, B] {
|
||||
return RIO.TraverseReader[context.Context, R](f)
|
||||
}
|
||||
560
v2/context/readerio/reader.go
Normal file
560
v2/context/readerio/reader.go
Normal file
@@ -0,0 +1,560 @@
|
||||
// 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 readerio
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
RIO "github.com/IBM/fp-go/v2/readerio"
|
||||
)
|
||||
|
||||
const (
|
||||
// useParallel is the feature flag to control if we use the parallel or the sequential implementation of ap
|
||||
useParallel = true
|
||||
)
|
||||
|
||||
// MonadMap transforms the success value of a [ReaderIO] using the provided function.
|
||||
// If the computation fails, the error is propagated unchanged.
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: The ReaderIO to transform
|
||||
// - f: The transformation function
|
||||
//
|
||||
// Returns a new ReaderIO with the transformed value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadMap[A, B any](fa ReaderIO[A], f func(A) B) ReaderIO[B] {
|
||||
return RIO.MonadMap(fa, f)
|
||||
}
|
||||
|
||||
// Map transforms the success value of a [ReaderIO] using the provided function.
|
||||
// This is the curried version of [MonadMap], useful for composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: The transformation function
|
||||
//
|
||||
// Returns a function that transforms a ReaderIO.
|
||||
//
|
||||
//go:inline
|
||||
func Map[A, B any](f func(A) B) Operator[A, B] {
|
||||
return RIO.Map[context.Context](f)
|
||||
}
|
||||
|
||||
// MonadMapTo replaces the success value of a [ReaderIO] with a constant value.
|
||||
// If the computation fails, the error is propagated unchanged.
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: The ReaderIO to transform
|
||||
// - b: The constant value to use
|
||||
//
|
||||
// Returns a new ReaderIO with the constant value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadMapTo[A, B any](fa ReaderIO[A], b B) ReaderIO[B] {
|
||||
return RIO.MonadMapTo(fa, b)
|
||||
}
|
||||
|
||||
// MapTo replaces the success value of a [ReaderIO] with a constant value.
|
||||
// This is the curried version of [MonadMapTo].
|
||||
//
|
||||
// Parameters:
|
||||
// - b: The constant value to use
|
||||
//
|
||||
// Returns a function that transforms a ReaderIO.
|
||||
//
|
||||
//go:inline
|
||||
func MapTo[A, B any](b B) Operator[A, B] {
|
||||
return RIO.MapTo[context.Context, A](b)
|
||||
}
|
||||
|
||||
// MonadChain sequences two [ReaderIO] computations, where the second depends on the result of the first.
|
||||
// If the first computation fails, the second is not executed.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The first ReaderIO
|
||||
// - f: Function that produces the second ReaderIO based on the first's result
|
||||
//
|
||||
// Returns a new ReaderIO representing the sequenced computation.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChain[A, B any](ma ReaderIO[A], f Kleisli[A, B]) ReaderIO[B] {
|
||||
return RIO.MonadChain(ma, f)
|
||||
}
|
||||
|
||||
// Chain sequences two [ReaderIO] computations, where the second depends on the result of the first.
|
||||
// This is the curried version of [MonadChain], useful for composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces the second ReaderIO based on the first's result
|
||||
//
|
||||
// Returns a function that sequences ReaderIO computations.
|
||||
//
|
||||
//go:inline
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||
return RIO.Chain(f)
|
||||
}
|
||||
|
||||
// MonadChainFirst sequences two [ReaderIO] computations but returns the result of the first.
|
||||
// The second computation is executed for its side effects only.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The first ReaderIO
|
||||
// - f: Function that produces the second ReaderIO
|
||||
//
|
||||
// Returns a ReaderIO with the result of the first computation.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirst[A, B any](ma ReaderIO[A], f Kleisli[A, B]) ReaderIO[A] {
|
||||
return RIO.MonadChainFirst(ma, f)
|
||||
}
|
||||
|
||||
// MonadTap executes a side-effect computation but returns the original value.
|
||||
// This is an alias for [MonadChainFirst] and is useful for operations like logging
|
||||
// or validation that should not affect the main computation flow.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The ReaderIO to tap
|
||||
// - f: Function that produces a side-effect ReaderIO
|
||||
//
|
||||
// Returns a ReaderIO with the original value after executing the side effect.
|
||||
//
|
||||
//go:inline
|
||||
func MonadTap[A, B any](ma ReaderIO[A], f Kleisli[A, B]) ReaderIO[A] {
|
||||
return RIO.MonadTap(ma, f)
|
||||
}
|
||||
|
||||
// ChainFirst sequences two [ReaderIO] computations but returns the result of the first.
|
||||
// This is the curried version of [MonadChainFirst].
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces the second ReaderIO
|
||||
//
|
||||
// Returns a function that sequences ReaderIO computations.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
||||
return RIO.ChainFirst(f)
|
||||
}
|
||||
|
||||
// Tap executes a side-effect computation but returns the original value.
|
||||
// This is the curried version of [MonadTap], an alias for [ChainFirst].
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces a side-effect ReaderIO
|
||||
//
|
||||
// Returns a function that taps ReaderIO computations.
|
||||
//
|
||||
//go:inline
|
||||
func Tap[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
||||
return RIO.Tap(f)
|
||||
}
|
||||
|
||||
// Of creates a [ReaderIO] that always succeeds with the given value.
|
||||
// This is the same as [Right] and represents the monadic return operation.
|
||||
//
|
||||
// Parameters:
|
||||
// - a: The value to wrap
|
||||
//
|
||||
// Returns a ReaderIO that always succeeds with the given value.
|
||||
//
|
||||
//go:inline
|
||||
func Of[A any](a A) ReaderIO[A] {
|
||||
return RIO.Of[context.Context](a)
|
||||
}
|
||||
|
||||
// MonadApPar implements parallel applicative application for [ReaderIO].
|
||||
// It executes the function and value computations in parallel where possible,
|
||||
// potentially improving performance for independent operations.
|
||||
//
|
||||
// Parameters:
|
||||
// - fab: ReaderIO containing a function
|
||||
// - fa: ReaderIO containing a value
|
||||
//
|
||||
// Returns a ReaderIO with the function applied to the value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadApPar[B, A any](fab ReaderIO[func(A) B], fa ReaderIO[A]) ReaderIO[B] {
|
||||
return RIO.MonadApPar(fab, fa)
|
||||
}
|
||||
|
||||
// MonadAp implements applicative application for [ReaderIO].
|
||||
// By default, it uses parallel execution ([MonadApPar]) but can be configured to use
|
||||
// sequential execution ([MonadApSeq]) via the useParallel constant.
|
||||
//
|
||||
// Parameters:
|
||||
// - fab: ReaderIO containing a function
|
||||
// - fa: ReaderIO containing a value
|
||||
//
|
||||
// Returns a ReaderIO with the function applied to the value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadAp[B, A any](fab ReaderIO[func(A) B], fa ReaderIO[A]) ReaderIO[B] {
|
||||
// dispatch to the configured version
|
||||
if useParallel {
|
||||
return MonadApPar(fab, fa)
|
||||
}
|
||||
return MonadApSeq(fab, fa)
|
||||
}
|
||||
|
||||
// MonadApSeq implements sequential applicative application for [ReaderIO].
|
||||
// It executes the function computation first, then the value computation.
|
||||
//
|
||||
// Parameters:
|
||||
// - fab: ReaderIO containing a function
|
||||
// - fa: ReaderIO containing a value
|
||||
//
|
||||
// Returns a ReaderIO with the function applied to the value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadApSeq[B, A any](fab ReaderIO[func(A) B], fa ReaderIO[A]) ReaderIO[B] {
|
||||
return RIO.MonadApSeq(fab, fa)
|
||||
}
|
||||
|
||||
// Ap applies a function wrapped in a [ReaderIO] to a value wrapped in a ReaderIO.
|
||||
// This is the curried version of [MonadAp], using the default execution mode.
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: ReaderIO containing a value
|
||||
//
|
||||
// Returns a function that applies a ReaderIO function to the value.
|
||||
//
|
||||
//go:inline
|
||||
func Ap[B, A any](fa ReaderIO[A]) Operator[func(A) B, B] {
|
||||
return RIO.Ap[B](fa)
|
||||
}
|
||||
|
||||
// ApSeq applies a function wrapped in a [ReaderIO] to a value sequentially.
|
||||
// This is the curried version of [MonadApSeq].
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: ReaderIO containing a value
|
||||
//
|
||||
// Returns a function that applies a ReaderIO function to the value sequentially.
|
||||
//
|
||||
//go:inline
|
||||
func ApSeq[B, A any](fa ReaderIO[A]) Operator[func(A) B, B] {
|
||||
return function.Bind2nd(MonadApSeq[B, A], fa)
|
||||
}
|
||||
|
||||
// ApPar applies a function wrapped in a [ReaderIO] to a value in parallel.
|
||||
// This is the curried version of [MonadApPar].
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: ReaderIO containing a value
|
||||
//
|
||||
// Returns a function that applies a ReaderIO function to the value in parallel.
|
||||
//
|
||||
//go:inline
|
||||
func ApPar[B, A any](fa ReaderIO[A]) Operator[func(A) B, B] {
|
||||
return function.Bind2nd(MonadApPar[B, A], fa)
|
||||
}
|
||||
|
||||
// Ask returns a [ReaderIO] that provides access to the context.
|
||||
// This is useful for accessing the [context.Context] within a computation.
|
||||
//
|
||||
// Returns a ReaderIO that produces the context.
|
||||
//
|
||||
//go:inline
|
||||
func Ask() ReaderIO[context.Context] {
|
||||
return RIO.Ask[context.Context]()
|
||||
}
|
||||
|
||||
// FromIO converts an [IO] into a [ReaderIO].
|
||||
// The IO computation always succeeds, so it's wrapped in Right.
|
||||
//
|
||||
// Parameters:
|
||||
// - t: The IO to convert
|
||||
//
|
||||
// Returns a ReaderIO that executes the IO and wraps the result in Right.
|
||||
//
|
||||
//go:inline
|
||||
func FromIO[A any](t IO[A]) ReaderIO[A] {
|
||||
return RIO.FromIO[context.Context](t)
|
||||
}
|
||||
|
||||
// FromReader converts a [Reader] into a [ReaderIO].
|
||||
// The Reader computation is lifted into the IO context, allowing it to be
|
||||
// composed with other ReaderIO operations.
|
||||
//
|
||||
// Parameters:
|
||||
// - t: The Reader to convert
|
||||
//
|
||||
// Returns a ReaderIO that executes the Reader and wraps the result in IO.
|
||||
//
|
||||
//go:inline
|
||||
func FromReader[A any](t Reader[context.Context, A]) ReaderIO[A] {
|
||||
return RIO.FromReader(t)
|
||||
}
|
||||
|
||||
// FromLazy converts a [Lazy] computation into a [ReaderIO].
|
||||
// The Lazy computation always succeeds, so it's wrapped in Right.
|
||||
// This is an alias for [FromIO] since Lazy and IO have the same structure.
|
||||
//
|
||||
// Parameters:
|
||||
// - t: The Lazy computation to convert
|
||||
//
|
||||
// Returns a ReaderIO that executes the Lazy computation and wraps the result in Right.
|
||||
//
|
||||
//go:inline
|
||||
func FromLazy[A any](t Lazy[A]) ReaderIO[A] {
|
||||
return RIO.FromIO[context.Context](t)
|
||||
}
|
||||
|
||||
// MonadChainIOK chains a function that returns an [IO] into a [ReaderIO] computation.
|
||||
// The IO computation always succeeds, so it's wrapped in Right.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The ReaderIO to chain from
|
||||
// - f: Function that produces an IO
|
||||
//
|
||||
// Returns a new ReaderIO with the chained IO computation.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainIOK[A, B any](ma ReaderIO[A], f func(A) IO[B]) ReaderIO[B] {
|
||||
return RIO.MonadChainIOK(ma, f)
|
||||
}
|
||||
|
||||
// ChainIOK chains a function that returns an [IO] into a [ReaderIO] computation.
|
||||
// This is the curried version of [MonadChainIOK].
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces an IO
|
||||
//
|
||||
// Returns a function that chains the IO-returning function.
|
||||
//
|
||||
//go:inline
|
||||
func ChainIOK[A, B any](f func(A) IO[B]) Operator[A, B] {
|
||||
return RIO.ChainIOK[context.Context](f)
|
||||
}
|
||||
|
||||
// MonadChainFirstIOK chains a function that returns an [IO] but keeps the original value.
|
||||
// The IO computation is executed for its side effects only.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The ReaderIO to chain from
|
||||
// - f: Function that produces an IO
|
||||
//
|
||||
// Returns a ReaderIO with the original value after executing the IO.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstIOK[A, B any](ma ReaderIO[A], f func(A) IO[B]) ReaderIO[A] {
|
||||
return RIO.MonadChainFirstIOK(ma, f)
|
||||
}
|
||||
|
||||
// MonadTapIOK chains a function that returns an [IO] but keeps the original value.
|
||||
// This is an alias for [MonadChainFirstIOK] and is useful for side effects like logging.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The ReaderIO to tap
|
||||
// - f: Function that produces an IO for side effects
|
||||
//
|
||||
// Returns a ReaderIO with the original value after executing the IO.
|
||||
//
|
||||
//go:inline
|
||||
func MonadTapIOK[A, B any](ma ReaderIO[A], f func(A) IO[B]) ReaderIO[A] {
|
||||
return RIO.MonadTapIOK(ma, f)
|
||||
}
|
||||
|
||||
// ChainFirstIOK chains a function that returns an [IO] but keeps the original value.
|
||||
// This is the curried version of [MonadChainFirstIOK].
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces an IO
|
||||
//
|
||||
// Returns a function that chains the IO-returning function.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
|
||||
return RIO.ChainFirstIOK[context.Context](f)
|
||||
}
|
||||
|
||||
// TapIOK chains a function that returns an [IO] but keeps the original value.
|
||||
// This is the curried version of [MonadTapIOK], an alias for [ChainFirstIOK].
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces an IO for side effects
|
||||
//
|
||||
// Returns a function that taps with IO-returning functions.
|
||||
//
|
||||
//go:inline
|
||||
func TapIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
|
||||
return RIO.TapIOK[context.Context](f)
|
||||
}
|
||||
|
||||
// Defer creates a [ReaderIO] by lazily generating a new computation each time it's executed.
|
||||
// This is useful for creating computations that should be re-evaluated on each execution.
|
||||
//
|
||||
// Parameters:
|
||||
// - gen: Lazy generator function that produces a ReaderIO
|
||||
//
|
||||
// Returns a ReaderIO that generates a fresh computation on each execution.
|
||||
//
|
||||
//go:inline
|
||||
func Defer[A any](gen Lazy[ReaderIO[A]]) ReaderIO[A] {
|
||||
return RIO.Defer(gen)
|
||||
}
|
||||
|
||||
// Memoize computes the value of the provided [ReaderIO] monad lazily but exactly once.
|
||||
// The context used to compute the value is the context of the first call, so do not use this
|
||||
// method if the value has a functional dependency on the content of the context.
|
||||
//
|
||||
// Parameters:
|
||||
// - rdr: The ReaderIO to memoize
|
||||
//
|
||||
// Returns a ReaderIO that caches its result after the first execution.
|
||||
//
|
||||
//go:inline
|
||||
func Memoize[A any](rdr ReaderIO[A]) ReaderIO[A] {
|
||||
return RIO.Memoize(rdr)
|
||||
}
|
||||
|
||||
// Flatten converts a nested [ReaderIO] into a flat [ReaderIO].
|
||||
// This is equivalent to [MonadChain] with the identity function.
|
||||
//
|
||||
// Parameters:
|
||||
// - rdr: The nested ReaderIO to flatten
|
||||
//
|
||||
// Returns a flattened ReaderIO.
|
||||
//
|
||||
//go:inline
|
||||
func Flatten[A any](rdr ReaderIO[ReaderIO[A]]) ReaderIO[A] {
|
||||
return RIO.Flatten(rdr)
|
||||
}
|
||||
|
||||
// MonadFlap applies a value to a function wrapped in a [ReaderIO].
|
||||
// This is the reverse of [MonadAp], useful in certain composition scenarios.
|
||||
//
|
||||
// Parameters:
|
||||
// - fab: ReaderIO containing a function
|
||||
// - a: The value to apply to the function
|
||||
//
|
||||
// Returns a ReaderIO with the function applied to the value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadFlap[B, A any](fab ReaderIO[func(A) B], a A) ReaderIO[B] {
|
||||
return RIO.MonadFlap(fab, a)
|
||||
}
|
||||
|
||||
// Flap applies a value to a function wrapped in a [ReaderIO].
|
||||
// This is the curried version of [MonadFlap].
|
||||
//
|
||||
// Parameters:
|
||||
// - a: The value to apply to the function
|
||||
//
|
||||
// Returns a function that applies the value to a ReaderIO function.
|
||||
//
|
||||
//go:inline
|
||||
func Flap[B, A any](a A) Operator[func(A) B, B] {
|
||||
return RIO.Flap[context.Context, B](a)
|
||||
}
|
||||
|
||||
// MonadChainReaderK chains a [ReaderIO] with a function that returns a [Reader].
|
||||
// The Reader is lifted into the ReaderIO context, allowing composition of
|
||||
// Reader and ReaderIO operations.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The ReaderIO to chain from
|
||||
// - f: Function that produces a Reader
|
||||
//
|
||||
// Returns a new ReaderIO with the chained Reader computation.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainReaderK[A, B any](ma ReaderIO[A], f reader.Kleisli[context.Context, A, B]) ReaderIO[B] {
|
||||
return RIO.MonadChainReaderK(ma, f)
|
||||
}
|
||||
|
||||
// ChainReaderK chains a [ReaderIO] with a function that returns a [Reader].
|
||||
// This is the curried version of [MonadChainReaderK].
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces a Reader
|
||||
//
|
||||
// Returns a function that chains Reader-returning functions.
|
||||
//
|
||||
//go:inline
|
||||
func ChainReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, B] {
|
||||
return RIO.ChainReaderK(f)
|
||||
}
|
||||
|
||||
// MonadChainFirstReaderK chains a function that returns a [Reader] but keeps the original value.
|
||||
// The Reader computation is executed for its side effects only.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The ReaderIO to chain from
|
||||
// - f: Function that produces a Reader
|
||||
//
|
||||
// Returns a ReaderIO with the original value after executing the Reader.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstReaderK[A, B any](ma ReaderIO[A], f reader.Kleisli[context.Context, A, B]) ReaderIO[A] {
|
||||
return RIO.MonadChainFirstReaderK(ma, f)
|
||||
}
|
||||
|
||||
// MonadTapReaderK chains a function that returns a [Reader] but keeps the original value.
|
||||
// This is an alias for [MonadChainFirstReaderK] and is useful for side effects.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The ReaderIO to tap
|
||||
// - f: Function that produces a Reader for side effects
|
||||
//
|
||||
// Returns a ReaderIO with the original value after executing the Reader.
|
||||
//
|
||||
//go:inline
|
||||
func MonadTapReaderK[A, B any](ma ReaderIO[A], f reader.Kleisli[context.Context, A, B]) ReaderIO[A] {
|
||||
return RIO.MonadTapReaderK(ma, f)
|
||||
}
|
||||
|
||||
// ChainFirstReaderK chains a function that returns a [Reader] but keeps the original value.
|
||||
// This is the curried version of [MonadChainFirstReaderK].
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces a Reader
|
||||
//
|
||||
// Returns a function that chains Reader-returning functions while preserving the original value.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, A] {
|
||||
return RIO.ChainFirstReaderK(f)
|
||||
}
|
||||
|
||||
// TapReaderK chains a function that returns a [Reader] but keeps the original value.
|
||||
// This is the curried version of [MonadTapReaderK], an alias for [ChainFirstReaderK].
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces a Reader for side effects
|
||||
//
|
||||
// Returns a function that taps with Reader-returning functions.
|
||||
//
|
||||
//go:inline
|
||||
func TapReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, A] {
|
||||
return RIO.TapReaderK(f)
|
||||
}
|
||||
|
||||
// Read executes a [ReaderIO] with a given context, returning the resulting [IO].
|
||||
// This is useful for providing the context dependency and obtaining an IO action
|
||||
// that can be executed later.
|
||||
//
|
||||
// Parameters:
|
||||
// - r: The context to provide to the ReaderIO
|
||||
//
|
||||
// Returns a function that converts a ReaderIO into an IO by applying the context.
|
||||
//
|
||||
//go:inline
|
||||
func Read[A any](r context.Context) func(ReaderIO[A]) IO[A] {
|
||||
return RIO.Read[A](r)
|
||||
}
|
||||
502
v2/context/readerio/reader_test.go
Normal file
502
v2/context/readerio/reader_test.go
Normal file
@@ -0,0 +1,502 @@
|
||||
// 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 readerio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
G "github.com/IBM/fp-go/v2/io"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMonadMap(t *testing.T) {
|
||||
rio := Of(5)
|
||||
doubled := MonadMap(rio, N.Mul(2))
|
||||
|
||||
result := doubled(context.Background())()
|
||||
assert.Equal(t, 10, result)
|
||||
}
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Of(1),
|
||||
Map(utils.Double),
|
||||
)
|
||||
|
||||
assert.Equal(t, 2, g(context.Background())())
|
||||
}
|
||||
|
||||
func TestMonadMapTo(t *testing.T) {
|
||||
rio := Of(42)
|
||||
replaced := MonadMapTo(rio, "constant")
|
||||
|
||||
result := replaced(context.Background())()
|
||||
assert.Equal(t, "constant", result)
|
||||
}
|
||||
|
||||
func TestMapTo(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of(42),
|
||||
MapTo[int]("constant"),
|
||||
)
|
||||
|
||||
assert.Equal(t, "constant", result(context.Background())())
|
||||
}
|
||||
|
||||
func TestMonadChain(t *testing.T) {
|
||||
rio1 := Of(5)
|
||||
result := MonadChain(rio1, func(n int) ReaderIO[int] {
|
||||
return Of(n * 3)
|
||||
})
|
||||
|
||||
assert.Equal(t, 15, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestChain(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of(5),
|
||||
Chain(func(n int) ReaderIO[int] {
|
||||
return Of(n * 3)
|
||||
}),
|
||||
)
|
||||
|
||||
assert.Equal(t, 15, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestMonadChainFirst(t *testing.T) {
|
||||
sideEffect := 0
|
||||
rio := Of(42)
|
||||
result := MonadChainFirst(rio, func(n int) ReaderIO[string] {
|
||||
sideEffect = n
|
||||
return Of("side effect")
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestChainFirst(t *testing.T) {
|
||||
sideEffect := 0
|
||||
result := F.Pipe1(
|
||||
Of(42),
|
||||
ChainFirst(func(n int) ReaderIO[string] {
|
||||
sideEffect = n
|
||||
return Of("side effect")
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestMonadTap(t *testing.T) {
|
||||
sideEffect := 0
|
||||
rio := Of(42)
|
||||
result := MonadTap(rio, func(n int) ReaderIO[func()] {
|
||||
sideEffect = n
|
||||
return Of(func() {})
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestTap(t *testing.T) {
|
||||
sideEffect := 0
|
||||
result := F.Pipe1(
|
||||
Of(42),
|
||||
Tap(func(n int) ReaderIO[func()] {
|
||||
sideEffect = n
|
||||
return Of(func() {})
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestOf(t *testing.T) {
|
||||
rio := Of(100)
|
||||
result := rio(context.Background())()
|
||||
|
||||
assert.Equal(t, 100, result)
|
||||
}
|
||||
|
||||
func TestMonadAp(t *testing.T) {
|
||||
fabIO := Of(N.Mul(2))
|
||||
faIO := Of(5)
|
||||
result := MonadAp(fabIO, faIO)
|
||||
|
||||
assert.Equal(t, 10, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestAp(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Of(utils.Double),
|
||||
Ap[int](Of(1)),
|
||||
)
|
||||
|
||||
assert.Equal(t, 2, g(context.Background())())
|
||||
}
|
||||
|
||||
func TestMonadApSeq(t *testing.T) {
|
||||
fabIO := Of(N.Add(10))
|
||||
faIO := Of(5)
|
||||
result := MonadApSeq(fabIO, faIO)
|
||||
|
||||
assert.Equal(t, 15, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestApSeq(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Of(N.Add(10)),
|
||||
ApSeq[int](Of(5)),
|
||||
)
|
||||
|
||||
assert.Equal(t, 15, g(context.Background())())
|
||||
}
|
||||
|
||||
func TestMonadApPar(t *testing.T) {
|
||||
fabIO := Of(N.Add(10))
|
||||
faIO := Of(5)
|
||||
result := MonadApPar(fabIO, faIO)
|
||||
|
||||
assert.Equal(t, 15, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestApPar(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Of(N.Add(10)),
|
||||
ApPar[int](Of(5)),
|
||||
)
|
||||
|
||||
assert.Equal(t, 15, g(context.Background())())
|
||||
}
|
||||
|
||||
func TestAsk(t *testing.T) {
|
||||
rio := Ask()
|
||||
ctx := context.WithValue(context.Background(), "key", "value")
|
||||
result := rio(ctx)()
|
||||
|
||||
assert.Equal(t, ctx, result)
|
||||
}
|
||||
|
||||
func TestFromIO(t *testing.T) {
|
||||
ioAction := G.Of(42)
|
||||
rio := FromIO(ioAction)
|
||||
|
||||
result := rio(context.Background())()
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
func TestFromReader(t *testing.T) {
|
||||
rdr := func(ctx context.Context) int {
|
||||
return 42
|
||||
}
|
||||
|
||||
rio := FromReader(rdr)
|
||||
result := rio(context.Background())()
|
||||
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
func TestFromLazy(t *testing.T) {
|
||||
lazy := func() int { return 42 }
|
||||
rio := FromLazy(lazy)
|
||||
|
||||
result := rio(context.Background())()
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
func TestMonadChainIOK(t *testing.T) {
|
||||
rio := Of(5)
|
||||
result := MonadChainIOK(rio, func(n int) G.IO[int] {
|
||||
return G.Of(n * 4)
|
||||
})
|
||||
|
||||
assert.Equal(t, 20, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestChainIOK(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of(5),
|
||||
ChainIOK(func(n int) G.IO[int] {
|
||||
return G.Of(n * 4)
|
||||
}),
|
||||
)
|
||||
|
||||
assert.Equal(t, 20, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestMonadChainFirstIOK(t *testing.T) {
|
||||
sideEffect := 0
|
||||
rio := Of(42)
|
||||
result := MonadChainFirstIOK(rio, func(n int) G.IO[string] {
|
||||
sideEffect = n
|
||||
return G.Of("side effect")
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestChainFirstIOK(t *testing.T) {
|
||||
sideEffect := 0
|
||||
result := F.Pipe1(
|
||||
Of(42),
|
||||
ChainFirstIOK(func(n int) G.IO[string] {
|
||||
sideEffect = n
|
||||
return G.Of("side effect")
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestMonadTapIOK(t *testing.T) {
|
||||
sideEffect := 0
|
||||
rio := Of(42)
|
||||
result := MonadTapIOK(rio, func(n int) G.IO[func()] {
|
||||
sideEffect = n
|
||||
return G.Of(func() {})
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestTapIOK(t *testing.T) {
|
||||
sideEffect := 0
|
||||
result := F.Pipe1(
|
||||
Of(42),
|
||||
TapIOK(func(n int) G.IO[func()] {
|
||||
sideEffect = n
|
||||
return G.Of(func() {})
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestDefer(t *testing.T) {
|
||||
counter := 0
|
||||
rio := Defer(func() ReaderIO[int] {
|
||||
counter++
|
||||
return Of(counter)
|
||||
})
|
||||
|
||||
result1 := rio(context.Background())()
|
||||
result2 := rio(context.Background())()
|
||||
|
||||
assert.Equal(t, 1, result1)
|
||||
assert.Equal(t, 2, result2)
|
||||
}
|
||||
|
||||
func TestMemoize(t *testing.T) {
|
||||
counter := 0
|
||||
rio := Of(0)
|
||||
memoized := Memoize(MonadMap(rio, func(int) int {
|
||||
counter++
|
||||
return counter
|
||||
}))
|
||||
|
||||
result1 := memoized(context.Background())()
|
||||
result2 := memoized(context.Background())()
|
||||
|
||||
assert.Equal(t, 1, result1)
|
||||
assert.Equal(t, 1, result2) // Same value, memoized
|
||||
}
|
||||
|
||||
func TestFlatten(t *testing.T) {
|
||||
nested := Of(Of(42))
|
||||
flattened := Flatten(nested)
|
||||
|
||||
result := flattened(context.Background())()
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
func TestMonadFlap(t *testing.T) {
|
||||
fabIO := Of(N.Mul(3))
|
||||
result := MonadFlap(fabIO, 7)
|
||||
|
||||
assert.Equal(t, 21, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestFlap(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of(N.Mul(3)),
|
||||
Flap[int](7),
|
||||
)
|
||||
|
||||
assert.Equal(t, 21, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestMonadChainReaderK(t *testing.T) {
|
||||
rio := Of(5)
|
||||
result := MonadChainReaderK(rio, func(n int) reader.Reader[context.Context, int] {
|
||||
return func(ctx context.Context) int { return n * 2 }
|
||||
})
|
||||
|
||||
assert.Equal(t, 10, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestChainReaderK(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of(5),
|
||||
ChainReaderK(func(n int) reader.Reader[context.Context, int] {
|
||||
return func(ctx context.Context) int { return n * 2 }
|
||||
}),
|
||||
)
|
||||
|
||||
assert.Equal(t, 10, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestMonadChainFirstReaderK(t *testing.T) {
|
||||
sideEffect := 0
|
||||
rio := Of(42)
|
||||
result := MonadChainFirstReaderK(rio, func(n int) reader.Reader[context.Context, string] {
|
||||
return func(ctx context.Context) string {
|
||||
sideEffect = n
|
||||
return "side effect"
|
||||
}
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestChainFirstReaderK(t *testing.T) {
|
||||
sideEffect := 0
|
||||
result := F.Pipe1(
|
||||
Of(42),
|
||||
ChainFirstReaderK(func(n int) reader.Reader[context.Context, string] {
|
||||
return func(ctx context.Context) string {
|
||||
sideEffect = n
|
||||
return "side effect"
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestMonadTapReaderK(t *testing.T) {
|
||||
sideEffect := 0
|
||||
rio := Of(42)
|
||||
result := MonadTapReaderK(rio, func(n int) reader.Reader[context.Context, func()] {
|
||||
return func(ctx context.Context) func() {
|
||||
sideEffect = n
|
||||
return func() {}
|
||||
}
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestTapReaderK(t *testing.T) {
|
||||
sideEffect := 0
|
||||
result := F.Pipe1(
|
||||
Of(42),
|
||||
TapReaderK(func(n int) reader.Reader[context.Context, func()] {
|
||||
return func(ctx context.Context) func() {
|
||||
sideEffect = n
|
||||
return func() {}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
rio := Of(42)
|
||||
ctx := context.Background()
|
||||
ioAction := Read[int](ctx)(rio)
|
||||
result := ioAction()
|
||||
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
func TestComplexPipeline(t *testing.T) {
|
||||
// Test a complex pipeline combining multiple operations
|
||||
result := F.Pipe3(
|
||||
Ask(),
|
||||
Map(func(ctx context.Context) int { return 5 }),
|
||||
Chain(func(n int) ReaderIO[int] {
|
||||
return Of(n * 2)
|
||||
}),
|
||||
Map(N.Add(10)),
|
||||
)
|
||||
|
||||
assert.Equal(t, 20, result(context.Background())()) // (5 * 2) + 10 = 20
|
||||
}
|
||||
|
||||
func TestFromIOWithChain(t *testing.T) {
|
||||
ioAction := G.Of(10)
|
||||
|
||||
result := F.Pipe1(
|
||||
FromIO(ioAction),
|
||||
Chain(func(n int) ReaderIO[int] {
|
||||
return Of(n + 5)
|
||||
}),
|
||||
)
|
||||
|
||||
assert.Equal(t, 15, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestTapWithLogging(t *testing.T) {
|
||||
// Simulate logging scenario
|
||||
logged := []int{}
|
||||
|
||||
result := F.Pipe3(
|
||||
Of(42),
|
||||
Tap(func(n int) ReaderIO[func()] {
|
||||
logged = append(logged, n)
|
||||
return Of(func() {})
|
||||
}),
|
||||
Map(N.Mul(2)),
|
||||
Tap(func(n int) ReaderIO[func()] {
|
||||
logged = append(logged, n)
|
||||
return Of(func() {})
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
assert.Equal(t, 84, value)
|
||||
assert.Equal(t, []int{42, 84}, logged)
|
||||
}
|
||||
69
v2/context/readerio/type.go
Normal file
69
v2/context/readerio/type.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// 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 readerio
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"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/readerio"
|
||||
)
|
||||
|
||||
type (
|
||||
// Lazy represents a deferred computation that produces a value of type A when executed.
|
||||
// The computation is not executed until explicitly invoked.
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
|
||||
// IO represents a side-effectful computation that produces a value of type A.
|
||||
// The computation is deferred and only executed when invoked.
|
||||
//
|
||||
// IO[A] is equivalent to func() A
|
||||
IO[A any] = io.IO[A]
|
||||
|
||||
// Reader represents a computation that depends on a context of type R.
|
||||
// This is used for dependency injection and accessing shared context.
|
||||
//
|
||||
// Reader[R, A] is equivalent to func(R) A
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
|
||||
// ReaderIO represents a context-dependent computation that performs side effects.
|
||||
// This is specialized to use [context.Context] as the context type.
|
||||
//
|
||||
// ReaderIO[A] is equivalent to func(context.Context) func() A
|
||||
ReaderIO[A any] = readerio.ReaderIO[context.Context, A]
|
||||
|
||||
// Kleisli represents a Kleisli arrow for the ReaderIO monad.
|
||||
// It is a function that takes a value of type A and returns a ReaderIO computation
|
||||
// that produces a value of type B.
|
||||
//
|
||||
// Kleisli arrows are used for composing monadic computations and are fundamental
|
||||
// to functional programming patterns involving effects and context.
|
||||
//
|
||||
// Kleisli[A, B] is equivalent to func(A) func(context.Context) func() B
|
||||
Kleisli[A, B any] = reader.Reader[A, ReaderIO[B]]
|
||||
|
||||
// Operator represents a transformation from one ReaderIO computation to another.
|
||||
// It takes a ReaderIO[A] and returns a ReaderIO[B], allowing for the composition
|
||||
// of context-dependent, side-effectful computations.
|
||||
//
|
||||
// Operators are useful for building pipelines of ReaderIO computations where
|
||||
// each step can depend on the previous computation's result.
|
||||
//
|
||||
// Operator[A, B] is equivalent to func(ReaderIO[A]) func(context.Context) func() B
|
||||
Operator[A, B any] = Kleisli[ReaderIO[A], B]
|
||||
)
|
||||
@@ -1,89 +0,0 @@
|
||||
// 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 readerioeither
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/internal/apply"
|
||||
"github.com/IBM/fp-go/v2/internal/chain"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
)
|
||||
|
||||
// Bind creates an empty context of type [S] to be used with the [Bind] operation
|
||||
func Do[S any](
|
||||
empty S,
|
||||
) ReaderIOEither[S] {
|
||||
return Of(empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) ReaderIOEither[T],
|
||||
) func(ReaderIOEither[S1]) ReaderIOEither[S2] {
|
||||
return chain.Bind(
|
||||
Chain[S1, S2],
|
||||
Map[T, S2],
|
||||
setter,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
func Let[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
) func(ReaderIOEither[S1]) ReaderIOEither[S2] {
|
||||
return functor.Let(
|
||||
Map[S1, S2],
|
||||
setter,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// LetTo attaches the a value to a context [S1] to produce a context [S2]
|
||||
func LetTo[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
) func(ReaderIOEither[S1]) ReaderIOEither[S2] {
|
||||
return functor.LetTo(
|
||||
Map[S1, S2],
|
||||
setter,
|
||||
b,
|
||||
)
|
||||
}
|
||||
|
||||
// BindTo initializes a new state [S1] from a value [T]
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
) Operator[T, S1] {
|
||||
return chain.BindTo(
|
||||
Map[T, S1],
|
||||
setter,
|
||||
)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
|
||||
func ApS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa ReaderIOEither[T],
|
||||
) func(ReaderIOEither[S1]) ReaderIOEither[S2] {
|
||||
return apply.ApS(
|
||||
Ap[S2, T],
|
||||
Map[S1, func(T) S2],
|
||||
setter,
|
||||
fa,
|
||||
)
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
mode: set
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/bind.go:27.21,29.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/bind.go:35.47,42.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/bind.go:48.47,54.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/bind.go:60.47,66.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/bind.go:71.46,76.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/bind.go:82.47,89.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/bracket.go:33.21,44.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/cancel.go:35.65,36.47 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/cancel.go:36.47,37.44 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/cancel.go:37.44,39.4 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/cancel.go:40.3,40.40 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/eq.go:42.84,44.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:18.91,20.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:24.93,26.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:30.101,32.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:36.103,38.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:43.36,48.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:53.36,58.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:63.36,68.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:71.98,76.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:79.101,84.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:87.101,92.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:95.129,96.68 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:96.68,102.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:106.132,107.68 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:107.68,113.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:117.132,118.68 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:118.68,124.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:129.113,131.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:135.115,137.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:143.40,150.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:156.40,163.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:169.40,176.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:179.126,185.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:188.129,194.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:197.129,203.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:206.185,207.76 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:207.76,215.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:219.188,220.76 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:220.76,228.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:232.188,233.76 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:233.76,241.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:246.125,248.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:252.127,254.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:261.44,270.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:277.44,286.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:293.44,302.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:305.154,312.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:315.157,322.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:325.157,332.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:335.241,336.84 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:336.84,346.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:350.244,351.84 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:351.84,361.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:365.244,366.84 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:366.84,376.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:381.137,383.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:387.139,389.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:397.48,408.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:416.48,427.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:435.48,446.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:449.182,457.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:460.185,468.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:471.185,479.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:482.297,483.92 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:483.92,495.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:499.300,500.92 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:500.92,512.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:516.300,517.92 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:517.92,529.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:534.149,536.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:540.151,542.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:551.52,564.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:573.52,586.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:595.52,608.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:611.210,620.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:623.213,632.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:635.213,644.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:647.353,648.100 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:648.100,662.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:666.356,667.100 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:667.100,681.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:685.356,686.100 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:686.100,700.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:705.161,707.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:711.163,713.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:723.56,738.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:748.56,763.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:773.56,788.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:791.238,801.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:804.241,814.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:817.241,827.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:830.409,831.108 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:831.108,847.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:851.412,852.108 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:852.108,868.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:872.412,873.108 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:873.108,889.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:894.173,896.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:900.175,902.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:913.60,930.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:941.60,958.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:969.60,986.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:989.266,1000.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1003.269,1014.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1017.269,1028.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1031.465,1032.116 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1032.116,1050.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1054.468,1055.116 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1055.116,1073.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1077.468,1078.116 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1078.116,1096.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1101.185,1103.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1107.187,1109.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1121.64,1140.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1152.64,1171.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1183.64,1202.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1205.294,1217.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1220.297,1232.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1235.297,1247.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1250.521,1251.124 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1251.124,1271.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1275.524,1276.124 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1276.124,1296.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1300.524,1301.124 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1301.124,1321.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1326.197,1328.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1332.199,1334.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1347.68,1368.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1381.68,1402.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1415.68,1436.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1439.322,1452.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1455.325,1468.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1471.325,1484.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1487.577,1488.132 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1488.132,1510.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1514.580,1515.132 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1515.132,1537.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1541.580,1542.132 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1542.132,1564.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1569.210,1571.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1575.212,1577.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1591.74,1614.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1628.74,1651.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1665.74,1688.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1691.356,1705.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1708.359,1722.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1725.359,1739.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1742.645,1743.144 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1743.144,1767.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1771.648,1772.144 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1772.144,1796.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1800.648,1801.144 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1801.144,1825.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/monoid.go:36.61,43.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/monoid.go:52.64,59.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/monoid.go:68.64,75.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/monoid.go:85.61,93.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/monoid.go:103.63,108.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:42.55,44.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:52.45,54.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:62.42,64.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:74.78,76.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:85.75,87.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:97.72,99.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:108.69,110.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:120.96,122.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:131.93,133.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:143.101,145.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:154.71,156.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:165.39,167.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:169.93,173.56 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:173.56,174.32 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:174.32,174.47 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:189.98,194.47 3 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:194.47,196.44 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:196.44,198.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:200.3,200.27 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:200.27,202.45 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:202.45,204.5 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:207.4,213.47 5 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:227.95,229.17 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:229.17,231.3 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:232.2,232.28 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:243.98,245.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:254.91,256.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:265.94,267.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:276.94,278.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:288.95,290.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:299.73,301.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:307.44,309.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:319.95,321.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:330.95,332.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:342.100,344.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:353.100,355.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:364.116,366.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:375.75,377.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:386.47,388.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:398.51,400.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:406.39,407.47 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:407.47,408.27 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:408.27,411.4 2 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:423.87,425.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:434.87,436.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:446.92,448.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:457.92,459.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:468.115,470.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:479.85,480.54 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:480.54,481.48 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:481.48,482.28 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:482.28,487.12 3 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:488.30,489.22 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:490.23,491.47 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:505.59,511.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:520.66,522.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:531.83,533.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:543.97,545.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:554.64,556.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:566.62,568.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:577.78,579.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:589.80,591.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:600.76,602.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:612.136,614.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:623.91,625.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:634.71,636.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/resource.go:58.151,63.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/semigroup.go:39.41,43.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/sync.go:46.78,54.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:31.89,39.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:48.103,56.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:65.71,67.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:75.112,83.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:92.124,100.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:108.94,110.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:120.95,128.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:137.92,145.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:148.106,156.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:165.74,167.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:170.118,178.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:181.115,189.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:192.127,200.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:203.97,205.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:215.95,223.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:232.92,240.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:243.106,251.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:260.74,262.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:265.115,273.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:276.127,284.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:287.118,295.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:304.97,306.2 1 0
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,72 +0,0 @@
|
||||
// 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"
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
RIOE "github.com/IBM/fp-go/v2/context/readerioeither"
|
||||
RIOEH "github.com/IBM/fp-go/v2/context/readerioeither/http"
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
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"
|
||||
LZ "github.com/IBM/fp-go/v2/lazy"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
func Requester(builder *R.Builder) RIOEH.Requester {
|
||||
|
||||
withBody := F.Curry3(func(data []byte, url string, method string) RIOE.ReaderIOEither[*http.Request] {
|
||||
return RIOE.TryCatch(func(ctx context.Context) func() (*http.Request, error) {
|
||||
return func() (*http.Request, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, 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) RIOE.ReaderIOEither[*http.Request] {
|
||||
return RIOE.TryCatch(func(ctx context.Context) func() (*http.Request, error) {
|
||||
return func() (*http.Request, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, nil)
|
||||
if err == nil {
|
||||
H.Monoid.Concat(req.Header, builder.GetHeaders())
|
||||
}
|
||||
return req, err
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return F.Pipe5(
|
||||
builder.GetBody(),
|
||||
O.Fold(LZ.Of(E.Of[error](withoutBody)), E.Map[error](withBody)),
|
||||
E.Ap[func(string) RIOE.ReaderIOEither[*http.Request]](builder.GetTargetURL()),
|
||||
E.Flap[error, RIOE.ReaderIOEither[*http.Request]](builder.GetMethod()),
|
||||
E.GetOrElse(RIOE.Left[*http.Request]),
|
||||
RIOE.Map(func(req *http.Request) *http.Request {
|
||||
req.Header = H.Monoid.Concat(req.Header, builder.GetHeaders())
|
||||
return req
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
// 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 (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
B "github.com/IBM/fp-go/v2/bytes"
|
||||
RIOE "github.com/IBM/fp-go/v2/context/readerioeither"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
H "github.com/IBM/fp-go/v2/http"
|
||||
IOE "github.com/IBM/fp-go/v2/ioeither"
|
||||
IOEF "github.com/IBM/fp-go/v2/ioeither/file"
|
||||
J "github.com/IBM/fp-go/v2/json"
|
||||
P "github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
type (
|
||||
// Requester is a reader that constructs a request
|
||||
Requester = RIOE.ReaderIOEither[*http.Request]
|
||||
|
||||
Client interface {
|
||||
// Do can send an HTTP request considering a context
|
||||
Do(Requester) RIOE.ReaderIOEither[*http.Response]
|
||||
}
|
||||
|
||||
client struct {
|
||||
delegate *http.Client
|
||||
doIOE func(*http.Request) IOE.IOEither[error, *http.Response]
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
// MakeRequest is an eitherized version of [http.NewRequestWithContext]
|
||||
MakeRequest = RIOE.Eitherize3(http.NewRequestWithContext)
|
||||
makeRequest = F.Bind13of3(MakeRequest)
|
||||
|
||||
// specialize
|
||||
MakeGetRequest = makeRequest("GET", nil)
|
||||
)
|
||||
|
||||
func (client client) Do(req Requester) RIOE.ReaderIOEither[*http.Response] {
|
||||
return F.Pipe1(
|
||||
req,
|
||||
RIOE.ChainIOEitherK(client.doIOE),
|
||||
)
|
||||
}
|
||||
|
||||
// MakeClient creates an HTTP client proxy
|
||||
func MakeClient(httpClient *http.Client) Client {
|
||||
return client{delegate: httpClient, doIOE: IOE.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) func(Requester) RIOE.ReaderIOEither[H.FullResponse] {
|
||||
return func(req Requester) RIOE.ReaderIOEither[H.FullResponse] {
|
||||
return F.Flow3(
|
||||
client.Do(req),
|
||||
IOE.ChainEitherK(H.ValidateResponse),
|
||||
IOE.Chain(func(resp *http.Response) IOE.IOEither[error, H.FullResponse] {
|
||||
return F.Pipe1(
|
||||
F.Pipe3(
|
||||
resp,
|
||||
H.GetBody,
|
||||
IOE.Of[error, io.ReadCloser],
|
||||
IOEF.ReadAll[io.ReadCloser],
|
||||
),
|
||||
IOE.Map[error](F.Bind1st(P.MakePair[*http.Response, []byte], resp)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ReadAll sends a request and reads the response as bytes
|
||||
func ReadAll(client Client) func(Requester) RIOE.ReaderIOEither[[]byte] {
|
||||
return F.Flow2(
|
||||
ReadFullResponse(client),
|
||||
RIOE.Map(H.Body),
|
||||
)
|
||||
}
|
||||
|
||||
// ReadText sends a request, reads the response and represents the response as a text string
|
||||
func ReadText(client Client) func(Requester) RIOE.ReaderIOEither[string] {
|
||||
return F.Flow2(
|
||||
ReadAll(client),
|
||||
RIOE.Map(B.ToString),
|
||||
)
|
||||
}
|
||||
|
||||
// ReadJson sends a request, reads the response and parses the response as JSON
|
||||
//
|
||||
// Deprecated: use [ReadJSON] instead
|
||||
func ReadJson[A any](client Client) func(Requester) RIOE.ReaderIOEither[A] {
|
||||
return ReadJSON[A](client)
|
||||
}
|
||||
|
||||
func readJSON(client Client) func(Requester) RIOE.ReaderIOEither[[]byte] {
|
||||
return F.Flow3(
|
||||
ReadFullResponse(client),
|
||||
RIOE.ChainFirstEitherK(F.Flow2(
|
||||
H.Response,
|
||||
H.ValidateJSONResponse,
|
||||
)),
|
||||
RIOE.Map(H.Body),
|
||||
)
|
||||
}
|
||||
|
||||
// ReadJSON sends a request, reads the response and parses the response as JSON
|
||||
func ReadJSON[A any](client Client) func(Requester) RIOE.ReaderIOEither[A] {
|
||||
return F.Flow2(
|
||||
readJSON(client),
|
||||
RIOE.ChainEitherK(J.Unmarshal[A]),
|
||||
)
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
// 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
H "net/http"
|
||||
|
||||
R "github.com/IBM/fp-go/v2/context/readerioeither"
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/errors"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
IOE "github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type PostItem struct {
|
||||
UserID uint `json:"userId"`
|
||||
Id uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
func getTitle(item PostItem) string {
|
||||
return item.Title
|
||||
}
|
||||
|
||||
type simpleRequestBuilder struct {
|
||||
method string
|
||||
url string
|
||||
headers H.Header
|
||||
}
|
||||
|
||||
func requestBuilder() simpleRequestBuilder {
|
||||
return simpleRequestBuilder{method: "GET"}
|
||||
}
|
||||
|
||||
func (b simpleRequestBuilder) WithURL(url string) simpleRequestBuilder {
|
||||
b.url = url
|
||||
return b
|
||||
}
|
||||
|
||||
func (b simpleRequestBuilder) WithHeader(key, value string) simpleRequestBuilder {
|
||||
if b.headers == nil {
|
||||
b.headers = make(H.Header)
|
||||
} else {
|
||||
b.headers = b.headers.Clone()
|
||||
}
|
||||
b.headers.Set(key, value)
|
||||
return b
|
||||
}
|
||||
|
||||
func (b simpleRequestBuilder) Build() R.ReaderIOEither[*H.Request] {
|
||||
return func(ctx context.Context) IOE.IOEither[error, *H.Request] {
|
||||
return IOE.TryCatchError(func() (*H.Request, error) {
|
||||
req, err := H.NewRequestWithContext(ctx, b.method, b.url, nil)
|
||||
if err == nil {
|
||||
req.Header = b.headers
|
||||
}
|
||||
return req, err
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendSingleRequest(t *testing.T) {
|
||||
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
req1 := MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1")
|
||||
|
||||
readItem := ReadJSON[PostItem](client)
|
||||
|
||||
resp1 := readItem(req1)
|
||||
|
||||
resE := resp1(context.TODO())()
|
||||
|
||||
fmt.Println(resE)
|
||||
}
|
||||
|
||||
// setHeaderUnsafe updates a header value in a request object by mutating the request object
|
||||
func setHeaderUnsafe(key, value string) func(*H.Request) *H.Request {
|
||||
return func(req *H.Request) *H.Request {
|
||||
req.Header.Set(key, value)
|
||||
return req
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendSingleRequestWithHeaderUnsafe(t *testing.T) {
|
||||
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
// this is not safe from a puristic perspective, because the map call mutates the request object
|
||||
req1 := F.Pipe2(
|
||||
"https://jsonplaceholder.typicode.com/posts/1",
|
||||
MakeGetRequest,
|
||||
R.Map(setHeaderUnsafe("Content-Type", "text/html")),
|
||||
)
|
||||
|
||||
readItem := ReadJSON[PostItem](client)
|
||||
|
||||
resp1 := F.Pipe2(
|
||||
req1,
|
||||
readItem,
|
||||
R.Map(getTitle),
|
||||
)
|
||||
|
||||
res := F.Pipe1(
|
||||
resp1(context.TODO())(),
|
||||
E.GetOrElse(errors.ToString),
|
||||
)
|
||||
|
||||
assert.Equal(t, "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", res)
|
||||
}
|
||||
|
||||
func TestSendSingleRequestWithHeaderSafe(t *testing.T) {
|
||||
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
// the request builder assembles config values to construct
|
||||
// the final http request. Each `With` step creates a copy of the settings
|
||||
// so the flow is pure
|
||||
request := requestBuilder().
|
||||
WithURL("https://jsonplaceholder.typicode.com/posts/1").
|
||||
WithHeader("Content-Type", "text/html").
|
||||
Build()
|
||||
|
||||
readItem := ReadJSON[PostItem](client)
|
||||
|
||||
response := F.Pipe2(
|
||||
request,
|
||||
readItem,
|
||||
R.Map(getTitle),
|
||||
)
|
||||
|
||||
res := F.Pipe1(
|
||||
response(context.TODO())(),
|
||||
E.GetOrElse(errors.ToString),
|
||||
)
|
||||
|
||||
assert.Equal(t, "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", res)
|
||||
}
|
||||
@@ -1,636 +0,0 @@
|
||||
// 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 readerioeither
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/errors"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/readerioeither"
|
||||
)
|
||||
|
||||
const (
|
||||
// useParallel is the feature flag to control if we use the parallel or the sequential implementation of ap
|
||||
useParallel = true
|
||||
)
|
||||
|
||||
// FromEither converts an [Either] into a [ReaderIOEither].
|
||||
// The resulting computation ignores the context and immediately returns the Either value.
|
||||
//
|
||||
// Parameters:
|
||||
// - e: The Either value to lift into ReaderIOEither
|
||||
//
|
||||
// Returns a ReaderIOEither that produces the given Either value.
|
||||
func FromEither[A any](e Either[A]) ReaderIOEither[A] {
|
||||
return readerioeither.FromEither[context.Context](e)
|
||||
}
|
||||
|
||||
// Left creates a [ReaderIOEither] that represents a failed computation with the given error.
|
||||
//
|
||||
// Parameters:
|
||||
// - l: The error value
|
||||
//
|
||||
// Returns a ReaderIOEither that always fails with the given error.
|
||||
func Left[A any](l error) ReaderIOEither[A] {
|
||||
return readerioeither.Left[context.Context, A](l)
|
||||
}
|
||||
|
||||
// Right creates a [ReaderIOEither] that represents a successful computation with the given value.
|
||||
//
|
||||
// Parameters:
|
||||
// - r: The success value
|
||||
//
|
||||
// Returns a ReaderIOEither that always succeeds with the given value.
|
||||
func Right[A any](r A) ReaderIOEither[A] {
|
||||
return readerioeither.Right[context.Context, error](r)
|
||||
}
|
||||
|
||||
// MonadMap transforms the success value of a [ReaderIOEither] using the provided function.
|
||||
// If the computation fails, the error is propagated unchanged.
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: The ReaderIOEither to transform
|
||||
// - f: The transformation function
|
||||
//
|
||||
// Returns a new ReaderIOEither with the transformed value.
|
||||
func MonadMap[A, B any](fa ReaderIOEither[A], f func(A) B) ReaderIOEither[B] {
|
||||
return readerioeither.MonadMap(fa, f)
|
||||
}
|
||||
|
||||
// Map transforms the success value of a [ReaderIOEither] using the provided function.
|
||||
// This is the curried version of [MonadMap], useful for composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: The transformation function
|
||||
//
|
||||
// Returns a function that transforms a ReaderIOEither.
|
||||
func Map[A, B any](f func(A) B) Operator[A, B] {
|
||||
return readerioeither.Map[context.Context, error](f)
|
||||
}
|
||||
|
||||
// MonadMapTo replaces the success value of a [ReaderIOEither] with a constant value.
|
||||
// If the computation fails, the error is propagated unchanged.
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: The ReaderIOEither to transform
|
||||
// - b: The constant value to use
|
||||
//
|
||||
// Returns a new ReaderIOEither with the constant value.
|
||||
func MonadMapTo[A, B any](fa ReaderIOEither[A], b B) ReaderIOEither[B] {
|
||||
return readerioeither.MonadMapTo(fa, b)
|
||||
}
|
||||
|
||||
// MapTo replaces the success value of a [ReaderIOEither] with a constant value.
|
||||
// This is the curried version of [MonadMapTo].
|
||||
//
|
||||
// Parameters:
|
||||
// - b: The constant value to use
|
||||
//
|
||||
// Returns a function that transforms a ReaderIOEither.
|
||||
func MapTo[A, B any](b B) Operator[A, B] {
|
||||
return readerioeither.MapTo[context.Context, error, A](b)
|
||||
}
|
||||
|
||||
// MonadChain sequences two [ReaderIOEither] computations, where the second depends on the result of the first.
|
||||
// If the first computation fails, the second is not executed.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The first ReaderIOEither
|
||||
// - f: Function that produces the second ReaderIOEither based on the first's result
|
||||
//
|
||||
// Returns a new ReaderIOEither representing the sequenced computation.
|
||||
func MonadChain[A, B any](ma ReaderIOEither[A], f func(A) ReaderIOEither[B]) ReaderIOEither[B] {
|
||||
return readerioeither.MonadChain(ma, f)
|
||||
}
|
||||
|
||||
// Chain sequences two [ReaderIOEither] computations, where the second depends on the result of the first.
|
||||
// This is the curried version of [MonadChain], useful for composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces the second ReaderIOEither based on the first's result
|
||||
//
|
||||
// Returns a function that sequences ReaderIOEither computations.
|
||||
func Chain[A, B any](f func(A) ReaderIOEither[B]) Operator[A, B] {
|
||||
return readerioeither.Chain(f)
|
||||
}
|
||||
|
||||
// MonadChainFirst sequences two [ReaderIOEither] computations but returns the result of the first.
|
||||
// The second computation is executed for its side effects only.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The first ReaderIOEither
|
||||
// - f: Function that produces the second ReaderIOEither
|
||||
//
|
||||
// Returns a ReaderIOEither with the result of the first computation.
|
||||
func MonadChainFirst[A, B any](ma ReaderIOEither[A], f func(A) ReaderIOEither[B]) ReaderIOEither[A] {
|
||||
return readerioeither.MonadChainFirst(ma, f)
|
||||
}
|
||||
|
||||
// ChainFirst sequences two [ReaderIOEither] computations but returns the result of the first.
|
||||
// This is the curried version of [MonadChainFirst].
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces the second ReaderIOEither
|
||||
//
|
||||
// Returns a function that sequences ReaderIOEither computations.
|
||||
func ChainFirst[A, B any](f func(A) ReaderIOEither[B]) Operator[A, A] {
|
||||
return readerioeither.ChainFirst(f)
|
||||
}
|
||||
|
||||
// Of creates a [ReaderIOEither] that always succeeds with the given value.
|
||||
// This is the same as [Right] and represents the monadic return operation.
|
||||
//
|
||||
// Parameters:
|
||||
// - a: The value to wrap
|
||||
//
|
||||
// Returns a ReaderIOEither that always succeeds with the given value.
|
||||
func Of[A any](a A) ReaderIOEither[A] {
|
||||
return readerioeither.Of[context.Context, error](a)
|
||||
}
|
||||
|
||||
func withCancelCauseFunc[A any](cancel context.CancelCauseFunc, ma IOEither[A]) IOEither[A] {
|
||||
return function.Pipe3(
|
||||
ma,
|
||||
ioeither.Swap[error, A],
|
||||
ioeither.ChainFirstIOK[A](func(err error) func() any {
|
||||
return io.FromImpure(func() { cancel(err) })
|
||||
}),
|
||||
ioeither.Swap[A, error],
|
||||
)
|
||||
}
|
||||
|
||||
// MonadApPar implements parallel applicative application for [ReaderIOEither].
|
||||
// It executes both computations in parallel and creates a sub-context that will be canceled
|
||||
// if either operation fails. This provides automatic cancellation propagation.
|
||||
//
|
||||
// Parameters:
|
||||
// - fab: ReaderIOEither containing a function
|
||||
// - fa: ReaderIOEither containing a value
|
||||
//
|
||||
// Returns a ReaderIOEither with the function applied to the value.
|
||||
func MonadApPar[B, A any](fab ReaderIOEither[func(A) B], fa ReaderIOEither[A]) ReaderIOEither[B] {
|
||||
// context sensitive input
|
||||
cfab := WithContext(fab)
|
||||
cfa := WithContext(fa)
|
||||
|
||||
return func(ctx context.Context) IOEither[B] {
|
||||
// quick check for cancellation
|
||||
if err := context.Cause(ctx); err != nil {
|
||||
return ioeither.Left[B](err)
|
||||
}
|
||||
|
||||
return func() Either[B] {
|
||||
// quick check for cancellation
|
||||
if err := context.Cause(ctx); err != nil {
|
||||
return either.Left[B](err)
|
||||
}
|
||||
|
||||
// create sub-contexts for fa and fab, so they can cancel one other
|
||||
ctxSub, cancelSub := context.WithCancelCause(ctx)
|
||||
defer cancelSub(nil) // cancel has to be called in all paths
|
||||
|
||||
fabIOE := withCancelCauseFunc(cancelSub, cfab(ctxSub))
|
||||
faIOE := withCancelCauseFunc(cancelSub, cfa(ctxSub))
|
||||
|
||||
return ioeither.MonadApPar(fabIOE, faIOE)()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MonadAp implements applicative application for [ReaderIOEither].
|
||||
// By default, it uses parallel execution ([MonadApPar]) but can be configured to use
|
||||
// sequential execution ([MonadApSeq]) via the useParallel constant.
|
||||
//
|
||||
// Parameters:
|
||||
// - fab: ReaderIOEither containing a function
|
||||
// - fa: ReaderIOEither containing a value
|
||||
//
|
||||
// Returns a ReaderIOEither with the function applied to the value.
|
||||
func MonadAp[B, A any](fab ReaderIOEither[func(A) B], fa ReaderIOEither[A]) ReaderIOEither[B] {
|
||||
// dispatch to the configured version
|
||||
if useParallel {
|
||||
return MonadApPar(fab, fa)
|
||||
}
|
||||
return MonadApSeq(fab, fa)
|
||||
}
|
||||
|
||||
// MonadApSeq implements sequential applicative application for [ReaderIOEither].
|
||||
// It executes the function computation first, then the value computation.
|
||||
//
|
||||
// Parameters:
|
||||
// - fab: ReaderIOEither containing a function
|
||||
// - fa: ReaderIOEither containing a value
|
||||
//
|
||||
// Returns a ReaderIOEither with the function applied to the value.
|
||||
func MonadApSeq[B, A any](fab ReaderIOEither[func(A) B], fa ReaderIOEither[A]) ReaderIOEither[B] {
|
||||
return readerioeither.MonadApSeq(fab, fa)
|
||||
}
|
||||
|
||||
// Ap applies a function wrapped in a [ReaderIOEither] to a value wrapped in a ReaderIOEither.
|
||||
// This is the curried version of [MonadAp], using the default execution mode.
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: ReaderIOEither containing a value
|
||||
//
|
||||
// Returns a function that applies a ReaderIOEither function to the value.
|
||||
func Ap[B, A any](fa ReaderIOEither[A]) Operator[func(A) B, B] {
|
||||
return function.Bind2nd(MonadAp[B, A], fa)
|
||||
}
|
||||
|
||||
// ApSeq applies a function wrapped in a [ReaderIOEither] to a value sequentially.
|
||||
// This is the curried version of [MonadApSeq].
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: ReaderIOEither containing a value
|
||||
//
|
||||
// Returns a function that applies a ReaderIOEither function to the value sequentially.
|
||||
func ApSeq[B, A any](fa ReaderIOEither[A]) Operator[func(A) B, B] {
|
||||
return function.Bind2nd(MonadApSeq[B, A], fa)
|
||||
}
|
||||
|
||||
// ApPar applies a function wrapped in a [ReaderIOEither] to a value in parallel.
|
||||
// This is the curried version of [MonadApPar].
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: ReaderIOEither containing a value
|
||||
//
|
||||
// Returns a function that applies a ReaderIOEither function to the value in parallel.
|
||||
func ApPar[B, A any](fa ReaderIOEither[A]) Operator[func(A) B, B] {
|
||||
return function.Bind2nd(MonadApPar[B, A], fa)
|
||||
}
|
||||
|
||||
// FromPredicate creates a [ReaderIOEither] from a predicate function.
|
||||
// If the predicate returns true, the value is wrapped in Right; otherwise, Left with the error from onFalse.
|
||||
//
|
||||
// Parameters:
|
||||
// - pred: Predicate function to test the value
|
||||
// - onFalse: Function to generate an error when predicate fails
|
||||
//
|
||||
// Returns a function that converts a value to ReaderIOEither based on the predicate.
|
||||
func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) func(A) ReaderIOEither[A] {
|
||||
return readerioeither.FromPredicate[context.Context](pred, onFalse)
|
||||
}
|
||||
|
||||
// OrElse provides an alternative [ReaderIOEither] computation if the first one fails.
|
||||
// The alternative is only executed if the first computation results in a Left (error).
|
||||
//
|
||||
// Parameters:
|
||||
// - onLeft: Function that produces an alternative ReaderIOEither from the error
|
||||
//
|
||||
// Returns a function that provides fallback behavior for failed computations.
|
||||
func OrElse[A any](onLeft func(error) ReaderIOEither[A]) Operator[A, A] {
|
||||
return readerioeither.OrElse[context.Context](onLeft)
|
||||
}
|
||||
|
||||
// Ask returns a [ReaderIOEither] that provides access to the context.
|
||||
// This is useful for accessing the [context.Context] within a computation.
|
||||
//
|
||||
// Returns a ReaderIOEither that produces the context.
|
||||
func Ask() ReaderIOEither[context.Context] {
|
||||
return readerioeither.Ask[context.Context, error]()
|
||||
}
|
||||
|
||||
// MonadChainEitherK chains a function that returns an [Either] into a [ReaderIOEither] computation.
|
||||
// This is useful for integrating pure Either-returning functions into ReaderIOEither workflows.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The ReaderIOEither to chain from
|
||||
// - f: Function that produces an Either
|
||||
//
|
||||
// Returns a new ReaderIOEither with the chained computation.
|
||||
func MonadChainEitherK[A, B any](ma ReaderIOEither[A], f func(A) Either[B]) ReaderIOEither[B] {
|
||||
return readerioeither.MonadChainEitherK[context.Context](ma, f)
|
||||
}
|
||||
|
||||
// ChainEitherK chains a function that returns an [Either] into a [ReaderIOEither] computation.
|
||||
// This is the curried version of [MonadChainEitherK].
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces an Either
|
||||
//
|
||||
// Returns a function that chains the Either-returning function.
|
||||
func ChainEitherK[A, B any](f func(A) Either[B]) func(ma ReaderIOEither[A]) ReaderIOEither[B] {
|
||||
return readerioeither.ChainEitherK[context.Context](f)
|
||||
}
|
||||
|
||||
// MonadChainFirstEitherK chains a function that returns an [Either] but keeps the original value.
|
||||
// The Either-returning function is executed for its validation/side effects only.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The ReaderIOEither to chain from
|
||||
// - f: Function that produces an Either
|
||||
//
|
||||
// Returns a ReaderIOEither with the original value if both computations succeed.
|
||||
func MonadChainFirstEitherK[A, B any](ma ReaderIOEither[A], f func(A) Either[B]) ReaderIOEither[A] {
|
||||
return readerioeither.MonadChainFirstEitherK[context.Context](ma, f)
|
||||
}
|
||||
|
||||
// ChainFirstEitherK chains a function that returns an [Either] but keeps the original value.
|
||||
// This is the curried version of [MonadChainFirstEitherK].
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces an Either
|
||||
//
|
||||
// Returns a function that chains the Either-returning function.
|
||||
func ChainFirstEitherK[A, B any](f func(A) Either[B]) func(ma ReaderIOEither[A]) ReaderIOEither[A] {
|
||||
return readerioeither.ChainFirstEitherK[context.Context](f)
|
||||
}
|
||||
|
||||
// ChainOptionK chains a function that returns an [Option] into a [ReaderIOEither] computation.
|
||||
// If the Option is None, the provided error function is called.
|
||||
//
|
||||
// Parameters:
|
||||
// - onNone: Function to generate an error when Option is None
|
||||
//
|
||||
// Returns a function that chains Option-returning functions into ReaderIOEither.
|
||||
func ChainOptionK[A, B any](onNone func() error) func(func(A) Option[B]) Operator[A, B] {
|
||||
return readerioeither.ChainOptionK[context.Context, A, B](onNone)
|
||||
}
|
||||
|
||||
// FromIOEither converts an [IOEither] into a [ReaderIOEither].
|
||||
// The resulting computation ignores the context.
|
||||
//
|
||||
// Parameters:
|
||||
// - t: The IOEither to convert
|
||||
//
|
||||
// Returns a ReaderIOEither that executes the IOEither.
|
||||
func FromIOEither[A any](t ioeither.IOEither[error, A]) ReaderIOEither[A] {
|
||||
return readerioeither.FromIOEither[context.Context](t)
|
||||
}
|
||||
|
||||
// FromIO converts an [IO] into a [ReaderIOEither].
|
||||
// The IO computation always succeeds, so it's wrapped in Right.
|
||||
//
|
||||
// Parameters:
|
||||
// - t: The IO to convert
|
||||
//
|
||||
// Returns a ReaderIOEither that executes the IO and wraps the result in Right.
|
||||
func FromIO[A any](t IO[A]) ReaderIOEither[A] {
|
||||
return readerioeither.FromIO[context.Context, error](t)
|
||||
}
|
||||
|
||||
// FromLazy converts a [Lazy] computation into a [ReaderIOEither].
|
||||
// The Lazy computation always succeeds, so it's wrapped in Right.
|
||||
// This is an alias for [FromIO] since Lazy and IO have the same structure.
|
||||
//
|
||||
// Parameters:
|
||||
// - t: The Lazy computation to convert
|
||||
//
|
||||
// Returns a ReaderIOEither that executes the Lazy computation and wraps the result in Right.
|
||||
func FromLazy[A any](t Lazy[A]) ReaderIOEither[A] {
|
||||
return readerioeither.FromIO[context.Context, error](t)
|
||||
}
|
||||
|
||||
// Never returns a [ReaderIOEither] that blocks indefinitely until the context is canceled.
|
||||
// This is useful for creating computations that wait for external cancellation signals.
|
||||
//
|
||||
// Returns a ReaderIOEither that waits for context cancellation and returns the cancellation error.
|
||||
func Never[A any]() ReaderIOEither[A] {
|
||||
return func(ctx context.Context) IOEither[A] {
|
||||
return func() Either[A] {
|
||||
<-ctx.Done()
|
||||
return either.Left[A](context.Cause(ctx))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MonadChainIOK chains a function that returns an [IO] into a [ReaderIOEither] computation.
|
||||
// The IO computation always succeeds, so it's wrapped in Right.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The ReaderIOEither to chain from
|
||||
// - f: Function that produces an IO
|
||||
//
|
||||
// Returns a new ReaderIOEither with the chained IO computation.
|
||||
func MonadChainIOK[A, B any](ma ReaderIOEither[A], f func(A) IO[B]) ReaderIOEither[B] {
|
||||
return readerioeither.MonadChainIOK(ma, f)
|
||||
}
|
||||
|
||||
// ChainIOK chains a function that returns an [IO] into a [ReaderIOEither] computation.
|
||||
// This is the curried version of [MonadChainIOK].
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces an IO
|
||||
//
|
||||
// Returns a function that chains the IO-returning function.
|
||||
func ChainIOK[A, B any](f func(A) IO[B]) func(ma ReaderIOEither[A]) ReaderIOEither[B] {
|
||||
return readerioeither.ChainIOK[context.Context, error](f)
|
||||
}
|
||||
|
||||
// MonadChainFirstIOK chains a function that returns an [IO] but keeps the original value.
|
||||
// The IO computation is executed for its side effects only.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The ReaderIOEither to chain from
|
||||
// - f: Function that produces an IO
|
||||
//
|
||||
// Returns a ReaderIOEither with the original value after executing the IO.
|
||||
func MonadChainFirstIOK[A, B any](ma ReaderIOEither[A], f func(A) IO[B]) ReaderIOEither[A] {
|
||||
return readerioeither.MonadChainFirstIOK(ma, f)
|
||||
}
|
||||
|
||||
// ChainFirstIOK chains a function that returns an [IO] but keeps the original value.
|
||||
// This is the curried version of [MonadChainFirstIOK].
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces an IO
|
||||
//
|
||||
// Returns a function that chains the IO-returning function.
|
||||
func ChainFirstIOK[A, B any](f func(A) IO[B]) func(ma ReaderIOEither[A]) ReaderIOEither[A] {
|
||||
return readerioeither.ChainFirstIOK[context.Context, error](f)
|
||||
}
|
||||
|
||||
// ChainIOEitherK chains a function that returns an [IOEither] into a [ReaderIOEither] computation.
|
||||
// This is useful for integrating IOEither-returning functions into ReaderIOEither workflows.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces an IOEither
|
||||
//
|
||||
// Returns a function that chains the IOEither-returning function.
|
||||
func ChainIOEitherK[A, B any](f func(A) ioeither.IOEither[error, B]) func(ma ReaderIOEither[A]) ReaderIOEither[B] {
|
||||
return readerioeither.ChainIOEitherK[context.Context](f)
|
||||
}
|
||||
|
||||
// Delay creates an operation that delays execution by the specified duration.
|
||||
// The computation waits for either the delay to expire or the context to be canceled.
|
||||
//
|
||||
// Parameters:
|
||||
// - delay: The duration to wait before executing the computation
|
||||
//
|
||||
// Returns a function that delays a ReaderIOEither computation.
|
||||
func Delay[A any](delay time.Duration) func(ma ReaderIOEither[A]) ReaderIOEither[A] {
|
||||
return func(ma ReaderIOEither[A]) ReaderIOEither[A] {
|
||||
return func(ctx context.Context) IOEither[A] {
|
||||
return func() Either[A] {
|
||||
// manage the timeout
|
||||
timeoutCtx, cancelTimeout := context.WithTimeout(ctx, delay)
|
||||
defer cancelTimeout()
|
||||
// whatever comes first
|
||||
select {
|
||||
case <-timeoutCtx.Done():
|
||||
return ma(ctx)()
|
||||
case <-ctx.Done():
|
||||
return either.Left[A](context.Cause(ctx))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Timer returns the current time after waiting for the specified delay.
|
||||
// This is useful for creating time-based computations.
|
||||
//
|
||||
// Parameters:
|
||||
// - delay: The duration to wait before returning the time
|
||||
//
|
||||
// Returns a ReaderIOEither that produces the current time after the delay.
|
||||
func Timer(delay time.Duration) ReaderIOEither[time.Time] {
|
||||
return function.Pipe2(
|
||||
io.Now,
|
||||
FromIO[time.Time],
|
||||
Delay[time.Time](delay),
|
||||
)
|
||||
}
|
||||
|
||||
// Defer creates a [ReaderIOEither] by lazily generating a new computation each time it's executed.
|
||||
// This is useful for creating computations that should be re-evaluated on each execution.
|
||||
//
|
||||
// Parameters:
|
||||
// - gen: Lazy generator function that produces a ReaderIOEither
|
||||
//
|
||||
// Returns a ReaderIOEither that generates a fresh computation on each execution.
|
||||
func Defer[A any](gen Lazy[ReaderIOEither[A]]) ReaderIOEither[A] {
|
||||
return readerioeither.Defer(gen)
|
||||
}
|
||||
|
||||
// TryCatch wraps a function that returns a tuple (value, error) into a [ReaderIOEither].
|
||||
// This is the standard way to convert Go error-returning functions into ReaderIOEither.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that takes a context and returns a function producing (value, error)
|
||||
//
|
||||
// Returns a ReaderIOEither that wraps the error-returning function.
|
||||
func TryCatch[A any](f func(context.Context) func() (A, error)) ReaderIOEither[A] {
|
||||
return readerioeither.TryCatch(f, errors.IdentityError)
|
||||
}
|
||||
|
||||
// MonadAlt provides an alternative [ReaderIOEither] if the first one fails.
|
||||
// The alternative is lazily evaluated only if needed.
|
||||
//
|
||||
// Parameters:
|
||||
// - first: The primary ReaderIOEither to try
|
||||
// - second: Lazy alternative ReaderIOEither to use if first fails
|
||||
//
|
||||
// Returns a ReaderIOEither that tries the first, then the second if first fails.
|
||||
func MonadAlt[A any](first ReaderIOEither[A], second Lazy[ReaderIOEither[A]]) ReaderIOEither[A] {
|
||||
return readerioeither.MonadAlt(first, second)
|
||||
}
|
||||
|
||||
// Alt provides an alternative [ReaderIOEither] if the first one fails.
|
||||
// This is the curried version of [MonadAlt].
|
||||
//
|
||||
// Parameters:
|
||||
// - second: Lazy alternative ReaderIOEither to use if first fails
|
||||
//
|
||||
// Returns a function that provides fallback behavior.
|
||||
func Alt[A any](second Lazy[ReaderIOEither[A]]) Operator[A, A] {
|
||||
return readerioeither.Alt(second)
|
||||
}
|
||||
|
||||
// Memoize computes the value of the provided [ReaderIOEither] monad lazily but exactly once.
|
||||
// The context used to compute the value is the context of the first call, so do not use this
|
||||
// method if the value has a functional dependency on the content of the context.
|
||||
//
|
||||
// Parameters:
|
||||
// - rdr: The ReaderIOEither to memoize
|
||||
//
|
||||
// Returns a ReaderIOEither that caches its result after the first execution.
|
||||
func Memoize[A any](rdr ReaderIOEither[A]) ReaderIOEither[A] {
|
||||
return readerioeither.Memoize(rdr)
|
||||
}
|
||||
|
||||
// Flatten converts a nested [ReaderIOEither] into a flat [ReaderIOEither].
|
||||
// This is equivalent to [MonadChain] with the identity function.
|
||||
//
|
||||
// Parameters:
|
||||
// - rdr: The nested ReaderIOEither to flatten
|
||||
//
|
||||
// Returns a flattened ReaderIOEither.
|
||||
func Flatten[A any](rdr ReaderIOEither[ReaderIOEither[A]]) ReaderIOEither[A] {
|
||||
return readerioeither.Flatten(rdr)
|
||||
}
|
||||
|
||||
// MonadFlap applies a value to a function wrapped in a [ReaderIOEither].
|
||||
// This is the reverse of [MonadAp], useful in certain composition scenarios.
|
||||
//
|
||||
// Parameters:
|
||||
// - fab: ReaderIOEither containing a function
|
||||
// - a: The value to apply to the function
|
||||
//
|
||||
// Returns a ReaderIOEither with the function applied to the value.
|
||||
func MonadFlap[B, A any](fab ReaderIOEither[func(A) B], a A) ReaderIOEither[B] {
|
||||
return readerioeither.MonadFlap(fab, a)
|
||||
}
|
||||
|
||||
// Flap applies a value to a function wrapped in a [ReaderIOEither].
|
||||
// This is the curried version of [MonadFlap].
|
||||
//
|
||||
// Parameters:
|
||||
// - a: The value to apply to the function
|
||||
//
|
||||
// Returns a function that applies the value to a ReaderIOEither function.
|
||||
func Flap[B, A any](a A) Operator[func(A) B, B] {
|
||||
return readerioeither.Flap[context.Context, error, B](a)
|
||||
}
|
||||
|
||||
// Fold handles both success and error cases of a [ReaderIOEither] by providing handlers for each.
|
||||
// Both handlers return ReaderIOEither, allowing for further composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - onLeft: Handler for error case
|
||||
// - onRight: Handler for success case
|
||||
//
|
||||
// Returns a function that folds a ReaderIOEither into a new ReaderIOEither.
|
||||
func Fold[A, B any](onLeft func(error) ReaderIOEither[B], onRight func(A) ReaderIOEither[B]) Operator[A, B] {
|
||||
return readerioeither.Fold(onLeft, onRight)
|
||||
}
|
||||
|
||||
// GetOrElse extracts the value from a [ReaderIOEither], providing a default via a function if it fails.
|
||||
// The result is a [ReaderIO] that always succeeds.
|
||||
//
|
||||
// Parameters:
|
||||
// - onLeft: Function to provide a default value from the error
|
||||
//
|
||||
// Returns a function that converts a ReaderIOEither to a ReaderIO.
|
||||
func GetOrElse[A any](onLeft func(error) ReaderIO[A]) func(ReaderIOEither[A]) ReaderIO[A] {
|
||||
return readerioeither.GetOrElse(onLeft)
|
||||
}
|
||||
|
||||
// OrLeft transforms the error of a [ReaderIOEither] using the provided function.
|
||||
// The success value is left unchanged.
|
||||
//
|
||||
// Parameters:
|
||||
// - onLeft: Function to transform the error
|
||||
//
|
||||
// Returns a function that transforms the error of a ReaderIOEither.
|
||||
func OrLeft[A any](onLeft func(error) ReaderIO[error]) Operator[A, A] {
|
||||
return readerioeither.OrLeft[A](onLeft)
|
||||
}
|
||||
@@ -1,532 +0,0 @@
|
||||
// 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 readerioeither
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFromEither(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with Right
|
||||
rightVal := E.Right[error](42)
|
||||
result := FromEither(rightVal)(ctx)()
|
||||
assert.Equal(t, E.Right[error](42), result)
|
||||
|
||||
// Test with Left
|
||||
err := errors.New("test error")
|
||||
leftVal := E.Left[int](err)
|
||||
result = FromEither(leftVal)(ctx)()
|
||||
assert.Equal(t, E.Left[int](err), result)
|
||||
}
|
||||
|
||||
func TestLeftRight(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test Left
|
||||
err := errors.New("test error")
|
||||
result := Left[int](err)(ctx)()
|
||||
assert.True(t, E.IsLeft(result))
|
||||
|
||||
// Test Right
|
||||
result = Right(42)(ctx)()
|
||||
assert.True(t, E.IsRight(result))
|
||||
val, _ := E.Unwrap(result)
|
||||
assert.Equal(t, 42, val)
|
||||
}
|
||||
|
||||
func TestOf(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
result := Of(42)(ctx)()
|
||||
assert.Equal(t, E.Right[error](42), result)
|
||||
}
|
||||
|
||||
func TestMonadMap(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with Right
|
||||
result := MonadMap(Right(42), func(x int) int { return x * 2 })(ctx)()
|
||||
assert.Equal(t, E.Right[error](84), result)
|
||||
|
||||
// Test with Left
|
||||
err := errors.New("test error")
|
||||
result = MonadMap(Left[int](err), func(x int) int { return x * 2 })(ctx)()
|
||||
assert.Equal(t, E.Left[int](err), result)
|
||||
}
|
||||
|
||||
func TestMonadMapTo(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with Right
|
||||
result := MonadMapTo(Right(42), "hello")(ctx)()
|
||||
assert.Equal(t, E.Right[error]("hello"), result)
|
||||
|
||||
// Test with Left
|
||||
err := errors.New("test error")
|
||||
result = MonadMapTo(Left[int](err), "hello")(ctx)()
|
||||
assert.Equal(t, E.Left[string](err), result)
|
||||
}
|
||||
|
||||
func TestMonadChain(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with Right
|
||||
result := MonadChain(Right(42), func(x int) ReaderIOEither[int] {
|
||||
return Right(x * 2)
|
||||
})(ctx)()
|
||||
assert.Equal(t, E.Right[error](84), result)
|
||||
|
||||
// Test with Left
|
||||
err := errors.New("test error")
|
||||
result = MonadChain(Left[int](err), func(x int) ReaderIOEither[int] {
|
||||
return Right(x * 2)
|
||||
})(ctx)()
|
||||
assert.Equal(t, E.Left[int](err), result)
|
||||
|
||||
// Test where function returns Left
|
||||
result = MonadChain(Right(42), func(x int) ReaderIOEither[int] {
|
||||
return Left[int](errors.New("chain error"))
|
||||
})(ctx)()
|
||||
assert.True(t, E.IsLeft(result))
|
||||
}
|
||||
|
||||
func TestMonadChainFirst(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with Right
|
||||
result := MonadChainFirst(Right(42), func(x int) ReaderIOEither[string] {
|
||||
return Right("ignored")
|
||||
})(ctx)()
|
||||
assert.Equal(t, E.Right[error](42), result)
|
||||
|
||||
// Test with Left in first
|
||||
err := errors.New("test error")
|
||||
result = MonadChainFirst(Left[int](err), func(x int) ReaderIOEither[string] {
|
||||
return Right("ignored")
|
||||
})(ctx)()
|
||||
assert.Equal(t, E.Left[int](err), result)
|
||||
|
||||
// Test with Left in second
|
||||
result = MonadChainFirst(Right(42), func(x int) ReaderIOEither[string] {
|
||||
return Left[string](errors.New("chain error"))
|
||||
})(ctx)()
|
||||
assert.True(t, E.IsLeft(result))
|
||||
}
|
||||
|
||||
func TestMonadApSeq(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with both Right
|
||||
fct := Right(func(x int) int { return x * 2 })
|
||||
val := Right(42)
|
||||
result := MonadApSeq(fct, val)(ctx)()
|
||||
assert.Equal(t, E.Right[error](84), result)
|
||||
|
||||
// Test with Left function
|
||||
err := errors.New("function error")
|
||||
fct = Left[func(int) int](err)
|
||||
result = MonadApSeq(fct, val)(ctx)()
|
||||
assert.Equal(t, E.Left[int](err), result)
|
||||
|
||||
// Test with Left value
|
||||
fct = Right(func(x int) int { return x * 2 })
|
||||
err = errors.New("value error")
|
||||
val = Left[int](err)
|
||||
result = MonadApSeq(fct, val)(ctx)()
|
||||
assert.Equal(t, E.Left[int](err), result)
|
||||
}
|
||||
|
||||
func TestMonadApPar(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with both Right
|
||||
fct := Right(func(x int) int { return x * 2 })
|
||||
val := Right(42)
|
||||
result := MonadApPar(fct, val)(ctx)()
|
||||
assert.Equal(t, E.Right[error](84), result)
|
||||
}
|
||||
|
||||
func TestFromPredicate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
pred := func(x int) bool { return x > 0 }
|
||||
onFalse := func(x int) error { return fmt.Errorf("value %d is not positive", x) }
|
||||
|
||||
// Test with predicate true
|
||||
result := FromPredicate(pred, onFalse)(42)(ctx)()
|
||||
assert.Equal(t, E.Right[error](42), result)
|
||||
|
||||
// Test with predicate false
|
||||
result = FromPredicate(pred, onFalse)(-1)(ctx)()
|
||||
assert.True(t, E.IsLeft(result))
|
||||
}
|
||||
|
||||
func TestAsk(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), "key", "value")
|
||||
result := Ask()(ctx)()
|
||||
assert.True(t, E.IsRight(result))
|
||||
retrievedCtx, _ := E.Unwrap(result)
|
||||
assert.Equal(t, "value", retrievedCtx.Value("key"))
|
||||
}
|
||||
|
||||
func TestMonadChainEitherK(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with Right
|
||||
result := MonadChainEitherK(Right(42), func(x int) E.Either[error, int] {
|
||||
return E.Right[error](x * 2)
|
||||
})(ctx)()
|
||||
assert.Equal(t, E.Right[error](84), result)
|
||||
|
||||
// Test with Left in Either
|
||||
result = MonadChainEitherK(Right(42), func(x int) E.Either[error, int] {
|
||||
return E.Left[int](errors.New("either error"))
|
||||
})(ctx)()
|
||||
assert.True(t, E.IsLeft(result))
|
||||
}
|
||||
|
||||
func TestMonadChainFirstEitherK(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with Right
|
||||
result := MonadChainFirstEitherK(Right(42), func(x int) E.Either[error, string] {
|
||||
return E.Right[error]("ignored")
|
||||
})(ctx)()
|
||||
assert.Equal(t, E.Right[error](42), result)
|
||||
|
||||
// Test with Left in Either
|
||||
result = MonadChainFirstEitherK(Right(42), func(x int) E.Either[error, string] {
|
||||
return E.Left[string](errors.New("either error"))
|
||||
})(ctx)()
|
||||
assert.True(t, E.IsLeft(result))
|
||||
}
|
||||
|
||||
func TestChainOptionKFunc(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
onNone := func() error { return errors.New("none error") }
|
||||
|
||||
// Test with Some
|
||||
chainFunc := ChainOptionK[int, int](onNone)
|
||||
result := chainFunc(func(x int) O.Option[int] {
|
||||
return O.Some(x * 2)
|
||||
})(Right(42))(ctx)()
|
||||
assert.Equal(t, E.Right[error](84), result)
|
||||
|
||||
// Test with None
|
||||
result = chainFunc(func(x int) O.Option[int] {
|
||||
return O.None[int]()
|
||||
})(Right(42))(ctx)()
|
||||
assert.True(t, E.IsLeft(result))
|
||||
}
|
||||
|
||||
func TestFromIOEither(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with Right
|
||||
ioe := func() E.Either[error, int] {
|
||||
return E.Right[error](42)
|
||||
}
|
||||
result := FromIOEither(ioe)(ctx)()
|
||||
assert.Equal(t, E.Right[error](42), result)
|
||||
|
||||
// Test with Left
|
||||
err := errors.New("test error")
|
||||
ioe = func() E.Either[error, int] {
|
||||
return E.Left[int](err)
|
||||
}
|
||||
result = FromIOEither(ioe)(ctx)()
|
||||
assert.Equal(t, E.Left[int](err), result)
|
||||
}
|
||||
|
||||
func TestFromIO(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
io := func() int { return 42 }
|
||||
result := FromIO(io)(ctx)()
|
||||
assert.Equal(t, E.Right[error](42), result)
|
||||
}
|
||||
|
||||
func TestFromLazy(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
lazy := func() int { return 42 }
|
||||
result := FromLazy(lazy)(ctx)()
|
||||
assert.Equal(t, E.Right[error](42), result)
|
||||
}
|
||||
|
||||
func TestNeverWithCancel(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Start Never in a goroutine
|
||||
done := make(chan E.Either[error, int])
|
||||
go func() {
|
||||
done <- Never[int]()(ctx)()
|
||||
}()
|
||||
|
||||
// Cancel the context
|
||||
cancel()
|
||||
|
||||
// Should receive cancellation error
|
||||
result := <-done
|
||||
assert.True(t, E.IsLeft(result))
|
||||
}
|
||||
|
||||
func TestMonadChainIOK(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with Right
|
||||
result := MonadChainIOK(Right(42), func(x int) func() int {
|
||||
return func() int { return x * 2 }
|
||||
})(ctx)()
|
||||
assert.Equal(t, E.Right[error](84), result)
|
||||
}
|
||||
|
||||
func TestMonadChainFirstIOK(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with Right
|
||||
result := MonadChainFirstIOK(Right(42), func(x int) func() string {
|
||||
return func() string { return "ignored" }
|
||||
})(ctx)()
|
||||
assert.Equal(t, E.Right[error](42), result)
|
||||
}
|
||||
|
||||
func TestDelayFunc(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
delay := 100 * time.Millisecond
|
||||
|
||||
start := time.Now()
|
||||
delayFunc := Delay[int](delay)
|
||||
result := delayFunc(Right(42))(ctx)()
|
||||
elapsed := time.Since(start)
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
assert.GreaterOrEqual(t, elapsed, delay)
|
||||
}
|
||||
|
||||
func TestDefer(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
count := 0
|
||||
|
||||
gen := func() ReaderIOEither[int] {
|
||||
count++
|
||||
return Right(count)
|
||||
}
|
||||
|
||||
deferred := Defer(gen)
|
||||
|
||||
// First call
|
||||
result1 := deferred(ctx)()
|
||||
assert.Equal(t, E.Right[error](1), result1)
|
||||
|
||||
// Second call should generate new value
|
||||
result2 := deferred(ctx)()
|
||||
assert.Equal(t, E.Right[error](2), result2)
|
||||
}
|
||||
|
||||
func TestTryCatch(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test success
|
||||
result := TryCatch(func(ctx context.Context) func() (int, error) {
|
||||
return func() (int, error) {
|
||||
return 42, nil
|
||||
}
|
||||
})(ctx)()
|
||||
assert.Equal(t, E.Right[error](42), result)
|
||||
|
||||
// Test error
|
||||
err := errors.New("test error")
|
||||
result = TryCatch(func(ctx context.Context) func() (int, error) {
|
||||
return func() (int, error) {
|
||||
return 0, err
|
||||
}
|
||||
})(ctx)()
|
||||
assert.Equal(t, E.Left[int](err), result)
|
||||
}
|
||||
|
||||
func TestMonadAlt(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with Right (alternative not called)
|
||||
result := MonadAlt(Right(42), func() ReaderIOEither[int] {
|
||||
return Right(99)
|
||||
})(ctx)()
|
||||
assert.Equal(t, E.Right[error](42), result)
|
||||
|
||||
// Test with Left (alternative called)
|
||||
err := errors.New("test error")
|
||||
result = MonadAlt(Left[int](err), func() ReaderIOEither[int] {
|
||||
return Right(99)
|
||||
})(ctx)()
|
||||
assert.Equal(t, E.Right[error](99), result)
|
||||
}
|
||||
|
||||
func TestMemoize(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
count := 0
|
||||
|
||||
rdr := Memoize(FromLazy(func() int {
|
||||
count++
|
||||
return count
|
||||
}))
|
||||
|
||||
// First call
|
||||
result1 := rdr(ctx)()
|
||||
assert.Equal(t, E.Right[error](1), result1)
|
||||
|
||||
// Second call should return memoized value
|
||||
result2 := rdr(ctx)()
|
||||
assert.Equal(t, E.Right[error](1), result2)
|
||||
}
|
||||
|
||||
func TestFlatten(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
nested := Right(Right(42))
|
||||
result := Flatten(nested)(ctx)()
|
||||
assert.Equal(t, E.Right[error](42), result)
|
||||
}
|
||||
|
||||
func TestMonadFlap(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
fab := Right(func(x int) int { return x * 2 })
|
||||
result := MonadFlap(fab, 42)(ctx)()
|
||||
assert.Equal(t, E.Right[error](84), result)
|
||||
}
|
||||
|
||||
func TestWithContext(t *testing.T) {
|
||||
// Test with non-canceled context
|
||||
ctx := context.Background()
|
||||
result := WithContext(Right(42))(ctx)()
|
||||
assert.Equal(t, E.Right[error](42), result)
|
||||
|
||||
// Test with canceled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
result = WithContext(Right(42))(ctx)()
|
||||
assert.True(t, E.IsLeft(result))
|
||||
}
|
||||
|
||||
func TestMonadAp(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with both Right
|
||||
fct := Right(func(x int) int { return x * 2 })
|
||||
val := Right(42)
|
||||
result := MonadAp(fct, val)(ctx)()
|
||||
assert.Equal(t, E.Right[error](84), result)
|
||||
}
|
||||
|
||||
// Test traverse functions
|
||||
func TestSequenceArray(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with all Right
|
||||
arr := []ReaderIOEither[int]{Right(1), Right(2), Right(3)}
|
||||
result := SequenceArray(arr)(ctx)()
|
||||
assert.True(t, E.IsRight(result))
|
||||
vals, _ := E.Unwrap(result)
|
||||
assert.Equal(t, []int{1, 2, 3}, vals)
|
||||
|
||||
// Test with one Left
|
||||
err := errors.New("test error")
|
||||
arr = []ReaderIOEither[int]{Right(1), Left[int](err), Right(3)}
|
||||
result = SequenceArray(arr)(ctx)()
|
||||
assert.True(t, E.IsLeft(result))
|
||||
}
|
||||
|
||||
func TestTraverseArray(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test transformation
|
||||
arr := []int{1, 2, 3}
|
||||
result := TraverseArray(func(x int) ReaderIOEither[int] {
|
||||
return Right(x * 2)
|
||||
})(arr)(ctx)()
|
||||
assert.True(t, E.IsRight(result))
|
||||
vals, _ := E.Unwrap(result)
|
||||
assert.Equal(t, []int{2, 4, 6}, vals)
|
||||
}
|
||||
|
||||
func TestSequenceRecord(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with all Right
|
||||
rec := map[string]ReaderIOEither[int]{
|
||||
"a": Right(1),
|
||||
"b": Right(2),
|
||||
}
|
||||
result := SequenceRecord(rec)(ctx)()
|
||||
assert.True(t, E.IsRight(result))
|
||||
vals, _ := E.Unwrap(result)
|
||||
assert.Equal(t, 1, vals["a"])
|
||||
assert.Equal(t, 2, vals["b"])
|
||||
}
|
||||
|
||||
func TestTraverseRecord(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test transformation
|
||||
rec := map[string]int{"a": 1, "b": 2}
|
||||
result := TraverseRecord[string](func(x int) ReaderIOEither[int] {
|
||||
return Right(x * 2)
|
||||
})(rec)(ctx)()
|
||||
assert.True(t, E.IsRight(result))
|
||||
vals, _ := E.Unwrap(result)
|
||||
assert.Equal(t, 2, vals["a"])
|
||||
assert.Equal(t, 4, vals["b"])
|
||||
}
|
||||
|
||||
// Test monoid functions
|
||||
func TestAltSemigroup(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
sg := AltSemigroup[int]()
|
||||
|
||||
// Test with Right (first succeeds)
|
||||
result := sg.Concat(Right(42), Right(99))(ctx)()
|
||||
assert.Equal(t, E.Right[error](42), result)
|
||||
|
||||
// Test with Left then Right (fallback)
|
||||
err := errors.New("test error")
|
||||
result = sg.Concat(Left[int](err), Right(99))(ctx)()
|
||||
assert.Equal(t, E.Right[error](99), result)
|
||||
}
|
||||
|
||||
// Test Do notation
|
||||
func TestDo(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
type State struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
result := Do(State{Value: 42})(ctx)()
|
||||
assert.True(t, E.IsRight(result))
|
||||
state, _ := E.Unwrap(result)
|
||||
assert.Equal(t, 42, state.Value)
|
||||
}
|
||||
@@ -1,306 +0,0 @@
|
||||
// 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 readerioeither
|
||||
|
||||
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 [[]A] into [[]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[[]B]].
|
||||
// This uses the default applicative behavior (parallel or sequential based on useParallel flag).
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that transforms each element into a ReaderIOEither
|
||||
//
|
||||
// Returns a function that transforms an array into a ReaderIOEither of an array.
|
||||
func TraverseArray[A, B any](f func(A) ReaderIOEither[B]) func([]A) ReaderIOEither[[]B] {
|
||||
return array.Traverse[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
Ap[[]B, B],
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// TraverseArrayWithIndex transforms an array [[]A] into [[]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[[]B]].
|
||||
// The transformation function receives both the index and the element.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that transforms each element with its index into a ReaderIOEither
|
||||
//
|
||||
// Returns a function that transforms an array into a ReaderIOEither of an array.
|
||||
func TraverseArrayWithIndex[A, B any](f func(int, A) ReaderIOEither[B]) func([]A) ReaderIOEither[[]B] {
|
||||
return array.TraverseWithIndex[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
Ap[[]B, B],
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// SequenceArray converts a homogeneous sequence of ReaderIOEither into a ReaderIOEither of sequence.
|
||||
// This is equivalent to TraverseArray with the identity function.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: Array of ReaderIOEither values
|
||||
//
|
||||
// Returns a ReaderIOEither containing an array of values.
|
||||
func SequenceArray[A any](ma []ReaderIOEither[A]) ReaderIOEither[[]A] {
|
||||
return TraverseArray(function.Identity[ReaderIOEither[A]])(ma)
|
||||
}
|
||||
|
||||
// TraverseRecord transforms a record [map[K]A] into [map[K]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[map[K]B]].
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that transforms each value into a ReaderIOEither
|
||||
//
|
||||
// Returns a function that transforms a map into a ReaderIOEither of a map.
|
||||
func TraverseRecord[K comparable, A, B any](f func(A) ReaderIOEither[B]) func(map[K]A) ReaderIOEither[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 [map[K]A] into [map[K]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[map[K]B]].
|
||||
// The transformation function receives both the key and the value.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that transforms each key-value pair into a ReaderIOEither
|
||||
//
|
||||
// Returns a function that transforms a map into a ReaderIOEither of a map.
|
||||
func TraverseRecordWithIndex[K comparable, A, B any](f func(K, A) ReaderIOEither[B]) func(map[K]A) ReaderIOEither[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 map of ReaderIOEither into a ReaderIOEither of map.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: Map of ReaderIOEither values
|
||||
//
|
||||
// Returns a ReaderIOEither containing a map of values.
|
||||
func SequenceRecord[K comparable, A any](ma map[K]ReaderIOEither[A]) ReaderIOEither[map[K]A] {
|
||||
return TraverseRecord[K](function.Identity[ReaderIOEither[A]])(ma)
|
||||
}
|
||||
|
||||
// MonadTraverseArraySeq transforms an array [[]A] into [[]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[[]B]].
|
||||
// This explicitly uses sequential execution.
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The array to traverse
|
||||
// - f: Function that transforms each element into a ReaderIOEither
|
||||
//
|
||||
// Returns a ReaderIOEither containing an array of transformed values.
|
||||
func MonadTraverseArraySeq[A, B any](as []A, f func(A) ReaderIOEither[B]) ReaderIOEither[[]B] {
|
||||
return array.MonadTraverse[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
ApSeq[[]B, B],
|
||||
as,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// TraverseArraySeq transforms an array [[]A] into [[]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[[]B]].
|
||||
// This is the curried version of [MonadTraverseArraySeq] with sequential execution.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that transforms each element into a ReaderIOEither
|
||||
//
|
||||
// Returns a function that transforms an array into a ReaderIOEither of an array.
|
||||
func TraverseArraySeq[A, B any](f func(A) ReaderIOEither[B]) func([]A) ReaderIOEither[[]B] {
|
||||
return array.Traverse[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
ApSeq[[]B, B],
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// TraverseArrayWithIndexSeq uses transforms an array [[]A] into [[]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[[]B]]
|
||||
func TraverseArrayWithIndexSeq[A, B any](f func(int, A) ReaderIOEither[B]) func([]A) ReaderIOEither[[]B] {
|
||||
return array.TraverseWithIndex[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
ApSeq[[]B, B],
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// SequenceArraySeq converts a homogeneous sequence of ReaderIOEither into a ReaderIOEither of sequence.
|
||||
// This explicitly uses sequential execution.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: Array of ReaderIOEither values
|
||||
//
|
||||
// Returns a ReaderIOEither containing an array of values.
|
||||
func SequenceArraySeq[A any](ma []ReaderIOEither[A]) ReaderIOEither[[]A] {
|
||||
return MonadTraverseArraySeq(ma, function.Identity[ReaderIOEither[A]])
|
||||
}
|
||||
|
||||
// MonadTraverseRecordSeq uses transforms a record [map[K]A] into [map[K]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[map[K]B]]
|
||||
func MonadTraverseRecordSeq[K comparable, A, B any](as map[K]A, f func(A) ReaderIOEither[B]) ReaderIOEither[map[K]B] {
|
||||
return record.MonadTraverse[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
ApSeq[map[K]B, B],
|
||||
as,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// TraverseRecordSeq uses transforms a record [map[K]A] into [map[K]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[map[K]B]]
|
||||
func TraverseRecordSeq[K comparable, A, B any](f func(A) ReaderIOEither[B]) func(map[K]A) ReaderIOEither[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 uses transforms a record [map[K]A] into [map[K]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[map[K]B]]
|
||||
func TraverseRecordWithIndexSeq[K comparable, A, B any](f func(K, A) ReaderIOEither[B]) func(map[K]A) ReaderIOEither[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
|
||||
func SequenceRecordSeq[K comparable, A any](ma map[K]ReaderIOEither[A]) ReaderIOEither[map[K]A] {
|
||||
return MonadTraverseRecordSeq(ma, function.Identity[ReaderIOEither[A]])
|
||||
}
|
||||
|
||||
// MonadTraverseArrayPar transforms an array [[]A] into [[]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[[]B]].
|
||||
// This explicitly uses parallel execution.
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The array to traverse
|
||||
// - f: Function that transforms each element into a ReaderIOEither
|
||||
//
|
||||
// Returns a ReaderIOEither containing an array of transformed values.
|
||||
func MonadTraverseArrayPar[A, B any](as []A, f func(A) ReaderIOEither[B]) ReaderIOEither[[]B] {
|
||||
return array.MonadTraverse[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
ApPar[[]B, B],
|
||||
as,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// TraverseArrayPar transforms an array [[]A] into [[]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[[]B]].
|
||||
// This is the curried version of [MonadTraverseArrayPar] with parallel execution.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that transforms each element into a ReaderIOEither
|
||||
//
|
||||
// Returns a function that transforms an array into a ReaderIOEither of an array.
|
||||
func TraverseArrayPar[A, B any](f func(A) ReaderIOEither[B]) func([]A) ReaderIOEither[[]B] {
|
||||
return array.Traverse[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
ApPar[[]B, B],
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// TraverseArrayWithIndexPar uses transforms an array [[]A] into [[]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[[]B]]
|
||||
func TraverseArrayWithIndexPar[A, B any](f func(int, A) ReaderIOEither[B]) func([]A) ReaderIOEither[[]B] {
|
||||
return array.TraverseWithIndex[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
ApPar[[]B, B],
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// SequenceArrayPar converts a homogeneous sequence of ReaderIOEither into a ReaderIOEither of sequence.
|
||||
// This explicitly uses parallel execution.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: Array of ReaderIOEither values
|
||||
//
|
||||
// Returns a ReaderIOEither containing an array of values.
|
||||
func SequenceArrayPar[A any](ma []ReaderIOEither[A]) ReaderIOEither[[]A] {
|
||||
return MonadTraverseArrayPar(ma, function.Identity[ReaderIOEither[A]])
|
||||
}
|
||||
|
||||
// TraverseRecordPar uses transforms a record [map[K]A] into [map[K]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[map[K]B]]
|
||||
func TraverseRecordPar[K comparable, A, B any](f func(A) ReaderIOEither[B]) func(map[K]A) ReaderIOEither[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 uses transforms a record [map[K]A] into [map[K]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[map[K]B]]
|
||||
func TraverseRecordWithIndexPar[K comparable, A, B any](f func(K, A) ReaderIOEither[B]) func(map[K]A) ReaderIOEither[map[K]B] {
|
||||
return record.TraverseWithIndex[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
ApPar[map[K]B, B],
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadTraverseRecordPar uses transforms a record [map[K]A] into [map[K]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[map[K]B]]
|
||||
func MonadTraverseRecordPar[K comparable, A, B any](as map[K]A, f func(A) ReaderIOEither[B]) ReaderIOEither[map[K]B] {
|
||||
return record.MonadTraverse[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
ApPar[map[K]B, B],
|
||||
as,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// SequenceRecordPar converts a homogeneous map of ReaderIOEither into a ReaderIOEither of map.
|
||||
// This explicitly uses parallel execution.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: Map of ReaderIOEither values
|
||||
//
|
||||
// Returns a ReaderIOEither containing a map of values.
|
||||
func SequenceRecordPar[K comparable, A any](ma map[K]ReaderIOEither[A]) ReaderIOEither[map[K]A] {
|
||||
return MonadTraverseRecordPar(ma, function.Identity[ReaderIOEither[A]])
|
||||
}
|
||||
374
v2/context/readerioresult/BENCHMARKS.md
Normal file
374
v2/context/readerioresult/BENCHMARKS.md
Normal file
@@ -0,0 +1,374 @@
|
||||
# ReaderIOResult Benchmarks
|
||||
|
||||
This document describes the benchmark suite for the `context/readerioeither` package and how to interpret the results to identify performance bottlenecks.
|
||||
|
||||
## Running Benchmarks
|
||||
|
||||
To run all benchmarks:
|
||||
```bash
|
||||
cd context/readerioeither
|
||||
go test -bench=. -benchmem
|
||||
```
|
||||
|
||||
To run specific benchmarks:
|
||||
```bash
|
||||
go test -bench=BenchmarkMap -benchmem
|
||||
go test -bench=BenchmarkChain -benchmem
|
||||
go test -bench=BenchmarkApPar -benchmem
|
||||
```
|
||||
|
||||
To run with more iterations for stable results:
|
||||
```bash
|
||||
go test -bench=. -benchmem -benchtime=100000x
|
||||
```
|
||||
|
||||
## Benchmark Categories
|
||||
|
||||
### 1. Core Constructors
|
||||
- `BenchmarkLeft` - Creating Left (error) values (~64ns, 2 allocs)
|
||||
- `BenchmarkRight` - Creating Right (success) values (~64ns, 2 allocs)
|
||||
- `BenchmarkOf` - Creating Right values via Of (~47ns, 2 allocs)
|
||||
|
||||
**Key Insights:**
|
||||
- All constructors allocate 2 times (64B total)
|
||||
- `Of` is slightly faster than `Right` due to inlining
|
||||
- Construction is very fast, suitable for hot paths
|
||||
|
||||
### 2. Conversion Operations
|
||||
- `BenchmarkFromEither_Right/Left` - Converting Either to ReaderIOResult (~70ns, 2 allocs)
|
||||
- `BenchmarkFromIO` - Converting IO to ReaderIOResult (~78ns, 3 allocs)
|
||||
- `BenchmarkFromIOEither_Right/Left` - Converting IOEither (~23ns, 1 alloc)
|
||||
|
||||
**Key Insights:**
|
||||
- FromIOEither is the fastest conversion (~23ns)
|
||||
- FromIO has an extra allocation due to wrapping
|
||||
- All conversions are lightweight
|
||||
|
||||
### 3. Execution Operations
|
||||
- `BenchmarkExecute_Right` - Executing Right computation (~37ns, 1 alloc)
|
||||
- `BenchmarkExecute_Left` - Executing Left computation (~48ns, 1 alloc)
|
||||
- `BenchmarkExecute_WithContext` - Executing with context (~42ns, 1 alloc)
|
||||
|
||||
**Key Insights:**
|
||||
- Execution is very fast with minimal allocations
|
||||
- Left path is slightly slower due to error handling
|
||||
- Context overhead is minimal (~5ns)
|
||||
|
||||
### 4. Functor Operations (Map)
|
||||
- `BenchmarkMonadMap_Right/Left` - Direct map (~135ns, 5 allocs)
|
||||
- `BenchmarkMap_Right/Left` - Curried map (~24ns, 1 alloc)
|
||||
- `BenchmarkMapTo_Right` - Replacing with constant (~69ns, 1 alloc)
|
||||
|
||||
**Key Insights:**
|
||||
- **Bottleneck:** MonadMap has 5 allocations (128B)
|
||||
- Curried Map is ~5x faster with fewer allocations
|
||||
- **Recommendation:** Use curried `Map` instead of `MonadMap`
|
||||
|
||||
### 5. Monad Operations (Chain)
|
||||
- `BenchmarkMonadChain_Right/Left` - Direct chain (~190ns, 6 allocs)
|
||||
- `BenchmarkChain_Right/Left` - Curried chain (~28ns, 1 alloc)
|
||||
- `BenchmarkChainFirst_Right/Left` - Chain preserving original (~27ns, 1 alloc)
|
||||
- `BenchmarkFlatten_Right/Left` - Removing nesting (~147ns, 7 allocs)
|
||||
|
||||
**Key Insights:**
|
||||
- **Bottleneck:** MonadChain has 6 allocations (160B)
|
||||
- Curried Chain is ~7x faster
|
||||
- ChainFirst is as fast as Chain
|
||||
- **Bottleneck:** Flatten has 7 allocations
|
||||
- **Recommendation:** Use curried `Chain` instead of `MonadChain`
|
||||
|
||||
### 6. Applicative Operations (Ap)
|
||||
- `BenchmarkMonadApSeq_RightRight` - Sequential apply (~281ns, 8 allocs)
|
||||
- `BenchmarkMonadApPar_RightRight` - Parallel apply (~49ns, 3 allocs)
|
||||
- `BenchmarkExecuteApSeq_RightRight` - Executing sequential (~1403ns, 8 allocs)
|
||||
- `BenchmarkExecuteApPar_RightRight` - Executing parallel (~5606ns, 61 allocs)
|
||||
|
||||
**Key Insights:**
|
||||
- **Major Bottleneck:** Parallel execution has 61 allocations (1896B)
|
||||
- Construction of ApPar is fast (~49ns), but execution is expensive
|
||||
- Sequential execution is faster for simple operations (~1.4μs vs ~5.6μs)
|
||||
- **Recommendation:** Use ApSeq for simple operations, ApPar only for truly independent, expensive computations
|
||||
- Parallel overhead includes context management and goroutine coordination
|
||||
|
||||
### 7. Alternative Operations
|
||||
- `BenchmarkAlt_RightRight/LeftRight` - Providing alternatives (~210-344ns, 6 allocs)
|
||||
- `BenchmarkOrElse_Right/Left` - Recovery from Left (~40-52ns, 2 allocs)
|
||||
|
||||
**Key Insights:**
|
||||
- Alt has significant overhead (6 allocations)
|
||||
- OrElse is much more efficient for error recovery
|
||||
- **Recommendation:** Prefer OrElse over Alt when possible
|
||||
|
||||
### 8. Chain Operations with Different Types
|
||||
- `BenchmarkChainEitherK_Right/Left` - Chaining Either (~25ns, 1 alloc)
|
||||
- `BenchmarkChainIOK_Right/Left` - Chaining IO (~55ns, 1 alloc)
|
||||
- `BenchmarkChainIOEitherK_Right/Left` - Chaining IOEither (~53ns, 1 alloc)
|
||||
|
||||
**Key Insights:**
|
||||
- All chain-K operations are efficient
|
||||
- ChainEitherK is fastest (pure transformation)
|
||||
- ChainIOK and ChainIOEitherK have similar performance
|
||||
|
||||
### 9. Context Operations
|
||||
- `BenchmarkAsk` - Accessing context (~52ns, 3 allocs)
|
||||
- `BenchmarkDefer` - Lazy generation (~34ns, 1 alloc)
|
||||
- `BenchmarkMemoize` - Caching results (~82ns, 4 allocs)
|
||||
|
||||
**Key Insights:**
|
||||
- Ask has 3 allocations for context wrapping
|
||||
- Defer is lightweight
|
||||
- Memoize has overhead but pays off for expensive computations
|
||||
|
||||
### 10. Delay Operations
|
||||
- `BenchmarkDelay_Construction` - Creating delayed computation (~19ns, 1 alloc)
|
||||
- `BenchmarkTimer_Construction` - Creating timer (~92ns, 3 allocs)
|
||||
|
||||
**Key Insights:**
|
||||
- Delay construction is very cheap
|
||||
- Timer has additional overhead for time operations
|
||||
|
||||
### 11. TryCatch Operations
|
||||
- `BenchmarkTryCatch_Success/Error` - Creating TryCatch (~33ns, 1 alloc)
|
||||
- `BenchmarkExecuteTryCatch_Success/Error` - Executing TryCatch (~3ns, 0 allocs)
|
||||
|
||||
**Key Insights:**
|
||||
- TryCatch construction is cheap
|
||||
- Execution is extremely fast with zero allocations
|
||||
- Excellent for wrapping Go error-returning functions
|
||||
|
||||
### 12. Pipeline Operations
|
||||
- `BenchmarkPipeline_Map_Right/Left` - Single Map in pipeline (~200-306ns, 9 allocs)
|
||||
- `BenchmarkPipeline_Chain_Right/Left` - Single Chain in pipeline (~155-217ns, 7 allocs)
|
||||
- `BenchmarkPipeline_Complex_Right/Left` - Multiple operations (~777-1039ns, 25 allocs)
|
||||
- `BenchmarkExecutePipeline_Complex_Right` - Executing complex pipeline (~533ns, 10 allocs)
|
||||
|
||||
**Key Insights:**
|
||||
- **Major Bottleneck:** Pipeline operations allocate heavily
|
||||
- Single Map: ~200ns with 9 allocations (224B)
|
||||
- Complex pipeline: ~900ns with 25 allocations (640B)
|
||||
- **Recommendation:** Avoid F.Pipe in hot paths, use direct function calls
|
||||
|
||||
### 13. Do-Notation Operations
|
||||
- `BenchmarkDo` - Creating empty context (~45ns, 2 allocs)
|
||||
- `BenchmarkBind_Right` - Binding values (~25ns, 1 alloc)
|
||||
- `BenchmarkLet_Right` - Pure computations (~23ns, 1 alloc)
|
||||
- `BenchmarkApS_Right` - Applicative binding (~99ns, 4 allocs)
|
||||
|
||||
**Key Insights:**
|
||||
- Do-notation operations are efficient
|
||||
- Bind and Let are very fast
|
||||
- ApS has more overhead (4 allocations)
|
||||
- Much better than either package's Bind (~130ns vs ~25ns)
|
||||
|
||||
### 14. Traverse Operations
|
||||
- `BenchmarkTraverseArray_Empty` - Empty array (~689ns, 13 allocs)
|
||||
- `BenchmarkTraverseArray_Small` - 3 elements (~1971ns, 37 allocs)
|
||||
- `BenchmarkTraverseArray_Medium` - 10 elements (~4386ns, 93 allocs)
|
||||
- `BenchmarkTraverseArraySeq_Small` - Sequential (~1885ns, 52 allocs)
|
||||
- `BenchmarkTraverseArrayPar_Small` - Parallel (~1362ns, 37 allocs)
|
||||
- `BenchmarkExecuteTraverseArraySeq_Small` - Executing sequential (~1080ns, 34 allocs)
|
||||
- `BenchmarkExecuteTraverseArrayPar_Small` - Executing parallel (~18560ns, 202 allocs)
|
||||
|
||||
**Key Insights:**
|
||||
- **Bottleneck:** Traverse operations allocate per element
|
||||
- Empty array still has 13 allocations (overhead)
|
||||
- Parallel construction is faster but execution is much slower
|
||||
- **Major Bottleneck:** Parallel execution: ~18.5μs with 202 allocations
|
||||
- Sequential execution is ~17x faster for small arrays
|
||||
- **Recommendation:** Use sequential traverse for small collections, parallel only for large, expensive operations
|
||||
|
||||
### 15. Record Operations
|
||||
- `BenchmarkTraverseRecord_Small` - 3 entries (~1444ns, 55 allocs)
|
||||
- `BenchmarkSequenceRecord_Small` - 3 entries (~1073ns, 54 allocs)
|
||||
|
||||
**Key Insights:**
|
||||
- Record operations have high allocation overhead
|
||||
- Similar performance to array traversal
|
||||
- Allocations scale with map size
|
||||
|
||||
### 16. Resource Management
|
||||
- `BenchmarkWithResource_Success` - Creating resource wrapper (~193ns, 8 allocs)
|
||||
- `BenchmarkExecuteWithResource_Success` - Executing with resource (varies)
|
||||
- `BenchmarkExecuteWithResource_ErrorInBody` - Error handling (varies)
|
||||
|
||||
**Key Insights:**
|
||||
- Resource management has 8 allocations for safety
|
||||
- Ensures proper cleanup even on errors
|
||||
- Overhead is acceptable for resource safety guarantees
|
||||
|
||||
### 17. Context Cancellation
|
||||
- `BenchmarkExecute_CanceledContext` - Executing with canceled context
|
||||
- `BenchmarkExecuteApPar_CanceledContext` - Parallel with canceled context
|
||||
|
||||
**Key Insights:**
|
||||
- Cancellation is handled efficiently
|
||||
- Minimal overhead for checking cancellation
|
||||
- ApPar respects cancellation properly
|
||||
|
||||
## Performance Bottlenecks Summary
|
||||
|
||||
### Critical Bottlenecks (>100ns or >5 allocations)
|
||||
|
||||
1. **Pipeline operations with F.Pipe** (~200-1000ns, 9-25 allocations)
|
||||
- **Impact:** High - commonly used pattern
|
||||
- **Mitigation:** Use direct function calls in hot paths
|
||||
- **Example:**
|
||||
```go
|
||||
// Slow (200ns, 9 allocs)
|
||||
result := F.Pipe1(rioe, Map[int](transform))
|
||||
|
||||
// Fast (24ns, 1 alloc)
|
||||
result := Map[int](transform)(rioe)
|
||||
```
|
||||
|
||||
2. **MonadMap and MonadChain** (~135-207ns, 5-6 allocations)
|
||||
- **Impact:** High - fundamental operations
|
||||
- **Mitigation:** Use curried versions (Map, Chain)
|
||||
- **Speedup:** 5-7x faster
|
||||
|
||||
3. **Parallel applicative execution** (~5.6μs, 61 allocations)
|
||||
- **Impact:** High when used
|
||||
- **Mitigation:** Use ApSeq for simple operations
|
||||
- **Note:** Only use ApPar for truly independent, expensive computations
|
||||
|
||||
4. **Parallel traverse execution** (~18.5μs, 202 allocations)
|
||||
- **Impact:** High for collections
|
||||
- **Mitigation:** Use sequential traverse for small collections
|
||||
- **Threshold:** Consider parallel only for >100 elements with expensive operations
|
||||
|
||||
5. **Alt operations** (~210-344ns, 6 allocations)
|
||||
- **Impact:** Medium
|
||||
- **Mitigation:** Use OrElse for error recovery (40-52ns, 2 allocs)
|
||||
|
||||
### Minor Bottlenecks (50-100ns or 3-4 allocations)
|
||||
|
||||
6. **Flatten operations** (~147ns, 7 allocations)
|
||||
- **Impact:** Low - less commonly used
|
||||
- **Mitigation:** Avoid unnecessary nesting
|
||||
|
||||
7. **Memoize** (~82ns, 4 allocations)
|
||||
- **Impact:** Low - overhead pays off for expensive computations
|
||||
- **Mitigation:** Only use for computations >1μs
|
||||
|
||||
8. **ApS in do-notation** (~99ns, 4 allocations)
|
||||
- **Impact:** Low
|
||||
- **Mitigation:** Use Let or Bind when possible
|
||||
|
||||
## Optimization Recommendations
|
||||
|
||||
### For Hot Paths
|
||||
|
||||
1. **Use curried functions over Monad* versions:**
|
||||
```go
|
||||
// Instead of:
|
||||
result := MonadMap(rioe, transform) // 135ns, 5 allocs
|
||||
|
||||
// Use:
|
||||
result := Map[int](transform)(rioe) // 24ns, 1 alloc
|
||||
```
|
||||
|
||||
2. **Avoid F.Pipe in performance-critical code:**
|
||||
```go
|
||||
// Instead of:
|
||||
result := F.Pipe3(rioe, Map(f1), Chain(f2), Map(f3)) // 1000ns, 25 allocs
|
||||
|
||||
// Use:
|
||||
result := Map(f3)(Chain(f2)(Map(f1)(rioe))) // Much faster
|
||||
```
|
||||
|
||||
3. **Use sequential operations for small collections:**
|
||||
```go
|
||||
// For arrays with <10 elements:
|
||||
result := TraverseArraySeq(f)(arr) // 1.9μs, 52 allocs
|
||||
|
||||
// Instead of:
|
||||
result := TraverseArrayPar(f)(arr) // 18.5μs, 202 allocs (when executed)
|
||||
```
|
||||
|
||||
4. **Prefer OrElse over Alt for error recovery:**
|
||||
```go
|
||||
// Instead of:
|
||||
result := Alt(alternative)(rioe) // 210-344ns, 6 allocs
|
||||
|
||||
// Use:
|
||||
result := OrElse(recover)(rioe) // 40-52ns, 2 allocs
|
||||
```
|
||||
|
||||
### For Context Operations
|
||||
|
||||
- Context operations are generally efficient
|
||||
- Ask has 3 allocations but is necessary for context access
|
||||
- Cancellation checking is fast and should be used liberally
|
||||
|
||||
### For Resource Management
|
||||
|
||||
- WithResource overhead (8 allocations) is acceptable for safety
|
||||
- Always use for resources that need cleanup
|
||||
- The RAII pattern prevents resource leaks
|
||||
|
||||
### Memory Considerations
|
||||
|
||||
- Most operations have 1-2 allocations
|
||||
- Monad* versions have 5-8 allocations
|
||||
- Pipeline operations allocate heavily
|
||||
- Parallel operations have significant allocation overhead
|
||||
- Traverse operations allocate per element
|
||||
|
||||
## Comparative Analysis
|
||||
|
||||
### Fastest Operations (<50ns, <2 allocations)
|
||||
- Constructors (Left, Right, Of)
|
||||
- Execution (Execute_Right/Left)
|
||||
- Curried operations (Map, Chain, ChainFirst)
|
||||
- TryCatch execution
|
||||
- Chain-K operations
|
||||
- Let and Bind in do-notation
|
||||
|
||||
### Medium Speed (50-200ns, 2-8 allocations)
|
||||
- Conversion operations
|
||||
- MonadMap, MonadChain
|
||||
- Flatten
|
||||
- Memoize
|
||||
- Resource management construction
|
||||
- Traverse construction
|
||||
|
||||
### Slower Operations (>200ns or >8 allocations)
|
||||
- Pipeline operations
|
||||
- Alt operations
|
||||
- Traverse operations (especially parallel)
|
||||
- Applicative operations (especially parallel execution)
|
||||
|
||||
## Parallel vs Sequential Trade-offs
|
||||
|
||||
### When to Use Sequential (ApSeq, TraverseArraySeq)
|
||||
- Small collections (<10 elements)
|
||||
- Fast operations (<1μs per element)
|
||||
- When allocations matter
|
||||
- Default choice for most use cases
|
||||
|
||||
### When to Use Parallel (ApPar, TraverseArrayPar)
|
||||
- Large collections (>100 elements)
|
||||
- Expensive operations (>10μs per element)
|
||||
- Independent computations
|
||||
- When latency matters more than throughput
|
||||
- I/O-bound operations
|
||||
|
||||
### Parallel Overhead
|
||||
- Construction: ~50ns, 3 allocs
|
||||
- Execution: +4-17μs, +40-160 allocs
|
||||
- Context management and goroutine coordination
|
||||
- Only worthwhile for truly expensive operations
|
||||
|
||||
## Conclusion
|
||||
|
||||
The `context/readerioeither` package is well-optimized with most operations completing in nanoseconds with minimal allocations. Key recommendations:
|
||||
|
||||
1. Use curried functions (Map, Chain) instead of Monad* versions (5-7x faster)
|
||||
2. Avoid F.Pipe in hot paths (5-10x slower)
|
||||
3. Use sequential operations by default; parallel only for expensive, independent computations
|
||||
4. Prefer OrElse over Alt for error recovery (5x faster)
|
||||
5. TryCatch is excellent for wrapping Go functions (near-zero execution cost)
|
||||
6. Context operations are efficient; use liberally
|
||||
7. Resource management overhead is acceptable for safety guarantees
|
||||
|
||||
For typical use cases, the performance is excellent. Only in extremely hot paths (millions of operations per second) should you consider the micro-optimizations suggested above.
|
||||
504
v2/context/readerioresult/FLIP_POINTFREE.md
Normal file
504
v2/context/readerioresult/FLIP_POINTFREE.md
Normal file
@@ -0,0 +1,504 @@
|
||||
# Sequence Functions and Point-Free Style Programming
|
||||
|
||||
This document explains how the `Sequence*` functions in the `context/readerioresult` package enable point-free style programming and improve code composition.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [What is Point-Free Style?](#what-is-point-free-style)
|
||||
2. [The Problem: Nested Function Application](#the-problem-nested-function-application)
|
||||
3. [The Solution: Sequence Functions](#the-solution-sequence-functions)
|
||||
4. [How Sequence Enables Point-Free Style](#how-sequence-enables-point-free-style)
|
||||
5. [Practical Benefits](#practical-benefits)
|
||||
6. [Examples](#examples)
|
||||
7. [Comparison: With and Without Sequence](#comparison-with-and-without-sequence)
|
||||
|
||||
## What is Point-Free Style?
|
||||
|
||||
Point-free style (also called tacit programming) is a programming paradigm where function definitions don't explicitly mention their arguments. Instead, functions are composed using combinators and higher-order functions.
|
||||
|
||||
**Traditional style (with points):**
|
||||
```go
|
||||
func double(x int) int {
|
||||
return x * 2
|
||||
}
|
||||
```
|
||||
|
||||
**Point-free style (without points):**
|
||||
```go
|
||||
var double = F.Flow2(
|
||||
N.Mul(2),
|
||||
identity,
|
||||
)
|
||||
```
|
||||
|
||||
The key benefit is that point-free style emphasizes **what** the function does (its transformation) rather than **how** it manipulates data.
|
||||
|
||||
## The Problem: Nested Function Application
|
||||
|
||||
In functional programming with monadic types like `ReaderIOResult`, we often have nested structures where we need to apply parameters in a specific order. Consider:
|
||||
|
||||
```go
|
||||
type ReaderIOResult[A any] = func(context.Context) func() Either[error, A]
|
||||
type Reader[R, A any] = func(R) A
|
||||
|
||||
// A computation that produces a Reader
|
||||
type Computation = ReaderIOResult[Reader[Config, int]]
|
||||
// Expands to: func(context.Context) func() Either[error, func(Config) int]
|
||||
```
|
||||
|
||||
To use this, we must apply parameters in this order:
|
||||
1. First, provide `context.Context`
|
||||
2. Then, execute the IO effect (call the function)
|
||||
3. Then, unwrap the `Either` to get the `Reader`
|
||||
4. Finally, provide the `Config`
|
||||
|
||||
This creates several problems:
|
||||
|
||||
### Problem 1: Awkward Parameter Order
|
||||
|
||||
```go
|
||||
computation := getComputation()
|
||||
ctx := context.Background()
|
||||
cfg := Config{Value: 42}
|
||||
|
||||
// Must apply in this specific order
|
||||
result := computation(ctx)() // Get Either[error, Reader[Config, int]]
|
||||
if reader, err := either.Unwrap(result); err == nil {
|
||||
value := reader(cfg) // Finally apply Config
|
||||
// use value
|
||||
}
|
||||
```
|
||||
|
||||
The `Config` parameter, which is often known early and stable, must be provided last. This prevents partial application and reuse.
|
||||
|
||||
### Problem 2: Cannot Partially Apply Dependencies
|
||||
|
||||
```go
|
||||
// Want to do this: create a reusable computation with Config baked in
|
||||
// But can't because Config comes last!
|
||||
withConfig := computation(cfg) // ❌ Doesn't work - cfg comes last, not first
|
||||
```
|
||||
|
||||
### Problem 3: Breaks Point-Free Composition
|
||||
|
||||
```go
|
||||
// Want to compose like this:
|
||||
var pipeline = F.Flow3(
|
||||
getComputation,
|
||||
applyConfig(cfg), // ❌ Can't do this - Config comes last
|
||||
processResult,
|
||||
)
|
||||
```
|
||||
|
||||
## The Solution: Sequence Functions
|
||||
|
||||
The `Sequence*` functions solve this by "flipping" or "sequencing" the nested structure, changing the order in which parameters are applied.
|
||||
|
||||
### SequenceReader
|
||||
|
||||
```go
|
||||
func SequenceReader[R, A any](
|
||||
ma ReaderIOResult[Reader[R, A]]
|
||||
) reader.Kleisli[context.Context, R, IOResult[A]]
|
||||
```
|
||||
|
||||
**Type transformation:**
|
||||
```
|
||||
From: func(context.Context) func() Either[error, func(R) A]
|
||||
To: func(R) func(context.Context) func() Either[error, A]
|
||||
```
|
||||
|
||||
Now `R` (the Reader's environment) comes **first**, before `context.Context`!
|
||||
|
||||
### SequenceReaderIO
|
||||
|
||||
```go
|
||||
func SequenceReaderIO[R, A any](
|
||||
ma ReaderIOResult[ReaderIO[R, A]]
|
||||
) reader.Kleisli[context.Context, R, IOResult[A]]
|
||||
```
|
||||
|
||||
**Type transformation:**
|
||||
```
|
||||
From: func(context.Context) func() Either[error, func(R) func() A]
|
||||
To: func(R) func(context.Context) func() Either[error, A]
|
||||
```
|
||||
|
||||
### SequenceReaderResult
|
||||
|
||||
```go
|
||||
func SequenceReaderResult[R, A any](
|
||||
ma ReaderIOResult[ReaderResult[R, A]]
|
||||
) reader.Kleisli[context.Context, R, IOResult[A]]
|
||||
```
|
||||
|
||||
**Type transformation:**
|
||||
```
|
||||
From: func(context.Context) func() Either[error, func(R) Either[error, A]]
|
||||
To: func(R) func(context.Context) func() Either[error, A]
|
||||
```
|
||||
|
||||
## How Sequence Enables Point-Free Style
|
||||
|
||||
### 1. Partial Application
|
||||
|
||||
By moving the environment parameter first, we can partially apply it:
|
||||
|
||||
```go
|
||||
type Config struct { Multiplier int }
|
||||
|
||||
computation := getComputation() // ReaderIOResult[Reader[Config, int]]
|
||||
sequenced := SequenceReader[Config, int](computation)
|
||||
|
||||
// Partially apply Config
|
||||
cfg := Config{Multiplier: 5}
|
||||
withConfig := sequenced(cfg) // ✅ Now we have ReaderIOResult[int]
|
||||
|
||||
// Reuse with different contexts
|
||||
result1 := withConfig(ctx1)()
|
||||
result2 := withConfig(ctx2)()
|
||||
```
|
||||
|
||||
### 2. Dependency Injection
|
||||
|
||||
Inject dependencies early in the pipeline:
|
||||
|
||||
```go
|
||||
type Database struct { ConnectionString string }
|
||||
|
||||
makeQuery := func(ctx context.Context) func() Either[error, func(Database) string] {
|
||||
// ... implementation
|
||||
}
|
||||
|
||||
// Sequence to enable DI
|
||||
queryWithDB := SequenceReader[Database, string](makeQuery)
|
||||
|
||||
// Inject database
|
||||
db := Database{ConnectionString: "localhost:5432"}
|
||||
query := queryWithDB(db) // ✅ Database injected
|
||||
|
||||
// Use query with any context
|
||||
result := query(context.Background())()
|
||||
```
|
||||
|
||||
### 3. Point-Free Composition
|
||||
|
||||
Build pipelines without mentioning intermediate values:
|
||||
|
||||
```go
|
||||
var pipeline = F.Flow3(
|
||||
getComputation, // ReaderIOResult[Reader[Config, int]]
|
||||
SequenceReader[Config, int], // func(Config) ReaderIOResult[int]
|
||||
applyConfig(cfg), // ReaderIOResult[int]
|
||||
)
|
||||
|
||||
// Or with partial application:
|
||||
var withConfig = F.Pipe1(
|
||||
getComputation(),
|
||||
SequenceReader[Config, int],
|
||||
)
|
||||
|
||||
result := withConfig(cfg)(ctx)()
|
||||
```
|
||||
|
||||
### 4. Reusable Computations
|
||||
|
||||
Create specialized versions of generic computations:
|
||||
|
||||
```go
|
||||
// Generic computation
|
||||
makeServiceInfo := func(ctx context.Context) func() Either[error, func(ServiceConfig) string] {
|
||||
// ... implementation
|
||||
}
|
||||
|
||||
sequenced := SequenceReader[ServiceConfig, string](makeServiceInfo)
|
||||
|
||||
// Create specialized versions
|
||||
authService := sequenced(ServiceConfig{Name: "Auth", Version: "1.0"})
|
||||
userService := sequenced(ServiceConfig{Name: "User", Version: "2.0"})
|
||||
|
||||
// Reuse across contexts
|
||||
authInfo := authService(ctx)()
|
||||
userInfo := userService(ctx)()
|
||||
```
|
||||
|
||||
## Practical Benefits
|
||||
|
||||
### 1. **Improved Testability**
|
||||
|
||||
Inject test dependencies easily:
|
||||
|
||||
```go
|
||||
// Production
|
||||
prodDB := Database{ConnectionString: "prod:5432"}
|
||||
prodQuery := queryWithDB(prodDB)
|
||||
|
||||
// Testing
|
||||
testDB := Database{ConnectionString: "test:5432"}
|
||||
testQuery := queryWithDB(testDB)
|
||||
|
||||
// Same computation, different dependencies
|
||||
```
|
||||
|
||||
### 2. **Better Separation of Concerns**
|
||||
|
||||
Separate configuration from execution:
|
||||
|
||||
```go
|
||||
// Configuration phase (pure, no effects)
|
||||
cfg := loadConfig()
|
||||
computation := sequenced(cfg)
|
||||
|
||||
// Execution phase (with effects)
|
||||
result := computation(ctx)()
|
||||
```
|
||||
|
||||
### 3. **Enhanced Composability**
|
||||
|
||||
Build complex pipelines from simple pieces:
|
||||
|
||||
```go
|
||||
var processUser = F.Flow4(
|
||||
loadUserConfig, // ReaderIOResult[Reader[Database, User]]
|
||||
SequenceReader, // func(Database) ReaderIOResult[User]
|
||||
applyDatabase(db), // ReaderIOResult[User]
|
||||
Chain(validateUser), // ReaderIOResult[ValidatedUser]
|
||||
)
|
||||
```
|
||||
|
||||
### 4. **Reduced Boilerplate**
|
||||
|
||||
No need to manually thread parameters:
|
||||
|
||||
```go
|
||||
// Without Sequence - manual threading
|
||||
func processWithConfig(cfg Config) ReaderIOResult[Result] {
|
||||
return func(ctx context.Context) func() Either[error, Result] {
|
||||
return func() Either[error, Result] {
|
||||
comp := getComputation()(ctx)()
|
||||
if reader, err := either.Unwrap(comp); err == nil {
|
||||
value := reader(cfg)
|
||||
// ... more processing
|
||||
}
|
||||
// ... error handling
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// With Sequence - point-free
|
||||
var processWithConfig = F.Flow2(
|
||||
getComputation,
|
||||
SequenceReader[Config, Result],
|
||||
)
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Database Query with Configuration
|
||||
|
||||
```go
|
||||
type QueryConfig struct {
|
||||
Timeout time.Duration
|
||||
MaxRows int
|
||||
}
|
||||
|
||||
type Database struct {
|
||||
ConnectionString string
|
||||
}
|
||||
|
||||
// Without Sequence
|
||||
func executeQueryOld(cfg QueryConfig, db Database) ReaderIOResult[[]Row] {
|
||||
return func(ctx context.Context) func() Either[error, []Row] {
|
||||
return func() Either[error, []Row] {
|
||||
// Must manually handle all parameters
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// With Sequence
|
||||
func makeQuery(ctx context.Context) func() Either[error, func(Database) []Row] {
|
||||
return func() Either[error, func(Database) []Row] {
|
||||
return Right[error](func(db Database) []Row {
|
||||
// Implementation
|
||||
return []Row{}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var executeQuery = F.Flow2(
|
||||
makeQuery,
|
||||
SequenceReader[Database, []Row],
|
||||
)
|
||||
|
||||
// Usage
|
||||
db := Database{ConnectionString: "localhost:5432"}
|
||||
query := executeQuery(db)
|
||||
result := query(ctx)()
|
||||
```
|
||||
|
||||
### Example 2: Multi-Service Architecture
|
||||
|
||||
```go
|
||||
type ServiceRegistry struct {
|
||||
AuthService AuthService
|
||||
UserService UserService
|
||||
EmailService EmailService
|
||||
}
|
||||
|
||||
// Create computations that depend on services
|
||||
makeAuthCheck := func(ctx context.Context) func() Either[error, func(ServiceRegistry) bool] {
|
||||
// ... implementation
|
||||
}
|
||||
|
||||
makeSendEmail := func(ctx context.Context) func() Either[error, func(ServiceRegistry) error] {
|
||||
// ... implementation
|
||||
}
|
||||
|
||||
// Sequence them
|
||||
authCheck := SequenceReader[ServiceRegistry, bool](makeAuthCheck)
|
||||
sendEmail := SequenceReader[ServiceRegistry, error](makeSendEmail)
|
||||
|
||||
// Inject services once
|
||||
registry := ServiceRegistry{ /* ... */ }
|
||||
checkAuth := authCheck(registry)
|
||||
sendMail := sendEmail(registry)
|
||||
|
||||
// Use with different contexts
|
||||
if isAuth, _ := either.Unwrap(checkAuth(ctx1)()); isAuth {
|
||||
sendMail(ctx2)()
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: Configuration-Driven Pipeline
|
||||
|
||||
```go
|
||||
type PipelineConfig struct {
|
||||
Stage1Config Stage1Config
|
||||
Stage2Config Stage2Config
|
||||
Stage3Config Stage3Config
|
||||
}
|
||||
|
||||
// Define stages
|
||||
stage1 := SequenceReader[Stage1Config, IntermediateResult1](makeStage1)
|
||||
stage2 := SequenceReader[Stage2Config, IntermediateResult2](makeStage2)
|
||||
stage3 := SequenceReader[Stage3Config, FinalResult](makeStage3)
|
||||
|
||||
// Build pipeline with configuration
|
||||
func buildPipeline(cfg PipelineConfig) ReaderIOResult[FinalResult] {
|
||||
return F.Pipe3(
|
||||
stage1(cfg.Stage1Config),
|
||||
Chain(func(r1 IntermediateResult1) ReaderIOResult[IntermediateResult2] {
|
||||
return stage2(cfg.Stage2Config)
|
||||
}),
|
||||
Chain(func(r2 IntermediateResult2) ReaderIOResult[FinalResult] {
|
||||
return stage3(cfg.Stage3Config)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// Execute pipeline
|
||||
cfg := loadPipelineConfig()
|
||||
pipeline := buildPipeline(cfg)
|
||||
result := pipeline(ctx)()
|
||||
```
|
||||
|
||||
## Comparison: With and Without Sequence
|
||||
|
||||
### Without Sequence (Imperative Style)
|
||||
|
||||
```go
|
||||
func processUser(userID string) ReaderIOResult[ProcessedUser] {
|
||||
return func(ctx context.Context) func() Either[error, ProcessedUser] {
|
||||
return func() Either[error, ProcessedUser] {
|
||||
// Get database
|
||||
dbComp := getDatabase()(ctx)()
|
||||
if dbReader, err := either.Unwrap(dbComp); err != nil {
|
||||
return Left[ProcessedUser](err)
|
||||
}
|
||||
db := dbReader(dbConfig)
|
||||
|
||||
// Get user
|
||||
userComp := getUser(userID)(ctx)()
|
||||
if userReader, err := either.Unwrap(userComp); err != nil {
|
||||
return Left[ProcessedUser](err)
|
||||
}
|
||||
user := userReader(db)
|
||||
|
||||
// Process user
|
||||
processComp := processUserData(user)(ctx)()
|
||||
if processReader, err := either.Unwrap(processComp); err != nil {
|
||||
return Left[ProcessedUser](err)
|
||||
}
|
||||
result := processReader(processingConfig)
|
||||
|
||||
return Right[error](result)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### With Sequence (Point-Free Style)
|
||||
|
||||
```go
|
||||
var processUser = func(userID string) ReaderIOResult[ProcessedUser] {
|
||||
return F.Pipe3(
|
||||
getDatabase,
|
||||
SequenceReader[DatabaseConfig, Database],
|
||||
applyConfig(dbConfig),
|
||||
Chain(func(db Database) ReaderIOResult[User] {
|
||||
return F.Pipe2(
|
||||
getUser(userID),
|
||||
SequenceReader[Database, User],
|
||||
applyDB(db),
|
||||
)
|
||||
}),
|
||||
Chain(func(user User) ReaderIOResult[ProcessedUser] {
|
||||
return F.Pipe2(
|
||||
processUserData(user),
|
||||
SequenceReader[ProcessingConfig, ProcessedUser],
|
||||
applyConfig(processingConfig),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Key Takeaways
|
||||
|
||||
1. **Sequence functions flip parameter order** to enable partial application
|
||||
2. **Dependencies come first**, making them easy to inject and test
|
||||
3. **Point-free style** becomes natural and readable
|
||||
4. **Composition** is enhanced through proper parameter ordering
|
||||
5. **Reusability** increases as computations can be specialized early
|
||||
6. **Testability** improves through easy dependency injection
|
||||
7. **Separation of concerns** is clearer (configuration vs. execution)
|
||||
|
||||
## When to Use Sequence
|
||||
|
||||
Use `Sequence*` functions when:
|
||||
|
||||
- ✅ You want to partially apply environment/configuration parameters
|
||||
- ✅ You're building reusable computations with injected dependencies
|
||||
- ✅ You need to test with different dependency implementations
|
||||
- ✅ You're composing complex pipelines in point-free style
|
||||
- ✅ You want to separate configuration from execution
|
||||
- ✅ You're working with nested Reader-like structures
|
||||
|
||||
Don't use `Sequence*` when:
|
||||
|
||||
- ❌ The original parameter order is already optimal
|
||||
- ❌ You're not doing any composition or partial application
|
||||
- ❌ The added abstraction doesn't provide value
|
||||
- ❌ The code is simpler without it
|
||||
|
||||
## Conclusion
|
||||
|
||||
The `Sequence*` functions are powerful tools for enabling point-free style programming in Go. By flipping the parameter order of nested monadic structures, they make it easy to:
|
||||
|
||||
- Partially apply dependencies
|
||||
- Build composable pipelines
|
||||
- Improve testability
|
||||
- Write more declarative code
|
||||
|
||||
While they add a layer of abstraction, the benefits in terms of code reusability, testability, and composability make them invaluable for functional programming in Go.
|
||||
724
v2/context/readerioresult/bind.go
Normal file
724
v2/context/readerioresult/bind.go
Normal file
@@ -0,0 +1,724 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/apply"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// Do creates an empty context of type [S] to be used with the [Bind] operation.
|
||||
// This is the starting point for do-notation style composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
// result := readerioeither.Do(State{})
|
||||
//
|
||||
//go:inline
|
||||
func Do[S any](
|
||||
empty S,
|
||||
) ReaderIOResult[S] {
|
||||
return RIOR.Of[context.Context](empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// This enables sequential composition where each step can depend on the results of previous steps
|
||||
// and access the context.Context from the environment.
|
||||
//
|
||||
// The setter function takes the result of the computation and returns a function that
|
||||
// updates the context from S1 to S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readerioeither.Do(State{}),
|
||||
// readerioeither.Bind(
|
||||
// func(user User) func(State) State {
|
||||
// return func(s State) State { s.User = user; return s }
|
||||
// },
|
||||
// func(s State) readerioeither.ReaderIOResult[User] {
|
||||
// return func(ctx context.Context) ioeither.IOEither[error, User] {
|
||||
// return ioeither.TryCatch(func() (User, error) {
|
||||
// return fetchUser(ctx)
|
||||
// })
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// readerioeither.Bind(
|
||||
// func(cfg Config) func(State) State {
|
||||
// return func(s State) State { s.Config = cfg; return s }
|
||||
// },
|
||||
// func(s State) readerioeither.ReaderIOResult[Config] {
|
||||
// // This can access s.User from the previous step
|
||||
// return func(ctx context.Context) ioeither.IOEither[error, Config] {
|
||||
// return ioeither.TryCatch(func() (Config, error) {
|
||||
// return fetchConfigForUser(ctx, s.User.ID)
|
||||
// })
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f Kleisli[S1, T],
|
||||
) Operator[S1, S2] {
|
||||
return RIOR.Bind(setter, f)
|
||||
}
|
||||
|
||||
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
//
|
||||
//go:inline
|
||||
func Let[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
) Operator[S1, S2] {
|
||||
return RIOR.Let[context.Context](setter, f)
|
||||
}
|
||||
|
||||
// LetTo attaches the a value to a context [S1] to produce a context [S2]
|
||||
//
|
||||
//go:inline
|
||||
func LetTo[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
) Operator[S1, S2] {
|
||||
return RIOR.LetTo[context.Context](setter, b)
|
||||
}
|
||||
|
||||
// BindTo initializes a new state [S1] from a value [T]
|
||||
//
|
||||
//go:inline
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
) Operator[T, S1] {
|
||||
return RIOR.BindTo[context.Context](setter)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
// the context and the value concurrently (using Applicative rather than Monad).
|
||||
// This allows independent computations to be combined without one depending on the result of the other.
|
||||
//
|
||||
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
|
||||
// and can conceptually run in parallel.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// // These operations are independent and can be combined with ApS
|
||||
// getUser := func(ctx context.Context) ioeither.IOEither[error, User] {
|
||||
// return ioeither.TryCatch(func() (User, error) {
|
||||
// return fetchUser(ctx)
|
||||
// })
|
||||
// }
|
||||
// getConfig := func(ctx context.Context) ioeither.IOEither[error, Config] {
|
||||
// return ioeither.TryCatch(func() (Config, error) {
|
||||
// return fetchConfig(ctx)
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readerioeither.Do(State{}),
|
||||
// readerioeither.ApS(
|
||||
// func(user User) func(State) State {
|
||||
// return func(s State) State { s.User = user; return s }
|
||||
// },
|
||||
// getUser,
|
||||
// ),
|
||||
// readerioeither.ApS(
|
||||
// func(cfg Config) func(State) State {
|
||||
// return func(s State) State { s.Config = cfg; return s }
|
||||
// },
|
||||
// getConfig,
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa ReaderIOResult[T],
|
||||
) Operator[S1, S2] {
|
||||
return apply.ApS(
|
||||
Ap,
|
||||
Map,
|
||||
setter,
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// ApSL attaches a value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines ApS with a lens, allowing you to use
|
||||
// optics to update nested structures in a more composable way.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// This eliminates the need to manually write setter functions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// userLens := lens.MakeLens(
|
||||
// func(s State) User { return s.User },
|
||||
// func(s State, u User) State { s.User = u; return s },
|
||||
// )
|
||||
//
|
||||
// getUser := func(ctx context.Context) ioeither.IOEither[error, User] {
|
||||
// return ioeither.TryCatch(func() (User, error) {
|
||||
// return fetchUser(ctx)
|
||||
// })
|
||||
// }
|
||||
// result := F.Pipe2(
|
||||
// readerioeither.Of(State{}),
|
||||
// readerioeither.ApSL(userLens, getUser),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa ReaderIOResult[T],
|
||||
) Operator[S, S] {
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// BindL is a variant of Bind that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. The function f receives the current value of the focused field and
|
||||
// returns a ReaderIOResult computation that produces an updated value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// userLens := lens.MakeLens(
|
||||
// func(s State) User { return s.User },
|
||||
// func(s State, u User) State { s.User = u; return s },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readerioeither.Do(State{}),
|
||||
// readerioeither.BindL(userLens, func(user User) readerioeither.ReaderIOResult[User] {
|
||||
// return func(ctx context.Context) ioeither.IOEither[error, User] {
|
||||
// return ioeither.TryCatch(func() (User, error) {
|
||||
// return fetchUser(ctx)
|
||||
// })
|
||||
// }
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func BindL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Kleisli[T, T],
|
||||
) Operator[S, S] {
|
||||
return RIOR.BindL(lens, f)
|
||||
}
|
||||
|
||||
// LetL is a variant of Let that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. The function f receives the current value of the focused field and
|
||||
// returns a new value (without wrapping in a ReaderIOResult).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// userLens := lens.MakeLens(
|
||||
// func(s State) User { return s.User },
|
||||
// func(s State, u User) State { s.User = u; return s },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readerioeither.Do(State{User: User{Name: "Alice"}}),
|
||||
// readerioeither.LetL(userLens, func(user User) User {
|
||||
// user.Name = "Bob"
|
||||
// return user
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func LetL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f func(T) T,
|
||||
) Operator[S, S] {
|
||||
return RIOR.LetL[context.Context](lens, f)
|
||||
}
|
||||
|
||||
// LetToL is a variant of LetTo that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. The value b is set directly to the focused field.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// userLens := lens.MakeLens(
|
||||
// func(s State) User { return s.User },
|
||||
// func(s State, u User) State { s.User = u; return s },
|
||||
// )
|
||||
//
|
||||
// newUser := User{Name: "Bob", ID: 123}
|
||||
// result := F.Pipe2(
|
||||
// readerioeither.Do(State{}),
|
||||
// readerioeither.LetToL(userLens, newUser),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func LetToL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
b T,
|
||||
) Operator[S, S] {
|
||||
return RIOR.LetToL[context.Context](lens, b)
|
||||
}
|
||||
|
||||
// BindIOEitherK is a variant of Bind that works with IOEither computations.
|
||||
// It lifts an IOEither Kleisli arrow into the ReaderIOResult context (with context.Context as environment).
|
||||
//
|
||||
// Parameters:
|
||||
// - setter: Updates state from S1 to S2 using result T
|
||||
// - f: An IOEither Kleisli arrow (S1 -> IOEither[error, T])
|
||||
//
|
||||
//go:inline
|
||||
func BindIOEitherK[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f ioresult.Kleisli[S1, T],
|
||||
) Operator[S1, S2] {
|
||||
return Bind(setter, F.Flow2(f, FromIOEither[T]))
|
||||
}
|
||||
|
||||
// BindIOResultK is a variant of Bind that works with IOResult computations.
|
||||
// This is an alias for BindIOEitherK for consistency with the Result naming convention.
|
||||
//
|
||||
// Parameters:
|
||||
// - setter: Updates state from S1 to S2 using result T
|
||||
// - f: An IOResult Kleisli arrow (S1 -> IOResult[T])
|
||||
//
|
||||
//go:inline
|
||||
func BindIOResultK[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f ioresult.Kleisli[S1, T],
|
||||
) Operator[S1, S2] {
|
||||
return Bind(setter, F.Flow2(f, FromIOResult[T]))
|
||||
}
|
||||
|
||||
// BindIOK is a variant of Bind that works with IO computations.
|
||||
// It lifts an IO Kleisli arrow into the ReaderIOResult context.
|
||||
//
|
||||
// Parameters:
|
||||
// - setter: Updates state from S1 to S2 using result T
|
||||
// - f: An IO Kleisli arrow (S1 -> IO[T])
|
||||
//
|
||||
//go:inline
|
||||
func BindIOK[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f io.Kleisli[S1, T],
|
||||
) Operator[S1, S2] {
|
||||
return Bind(setter, F.Flow2(f, FromIO[T]))
|
||||
}
|
||||
|
||||
// BindReaderK is a variant of Bind that works with Reader computations.
|
||||
// It lifts a Reader Kleisli arrow (with context.Context) into the ReaderIOResult context.
|
||||
//
|
||||
// Parameters:
|
||||
// - setter: Updates state from S1 to S2 using result T
|
||||
// - f: A Reader Kleisli arrow (S1 -> Reader[context.Context, T])
|
||||
//
|
||||
//go:inline
|
||||
func BindReaderK[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f reader.Kleisli[context.Context, S1, T],
|
||||
) Operator[S1, S2] {
|
||||
return Bind(setter, F.Flow2(f, FromReader[T]))
|
||||
}
|
||||
|
||||
// BindReaderIOK is a variant of Bind that works with ReaderIO computations.
|
||||
// It lifts a ReaderIO Kleisli arrow (with context.Context) into the ReaderIOResult context.
|
||||
//
|
||||
// Parameters:
|
||||
// - setter: Updates state from S1 to S2 using result T
|
||||
// - f: A ReaderIO Kleisli arrow (S1 -> ReaderIO[context.Context, T])
|
||||
//
|
||||
//go:inline
|
||||
func BindReaderIOK[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f readerio.Kleisli[context.Context, S1, T],
|
||||
) Operator[S1, S2] {
|
||||
return Bind(setter, F.Flow2(f, FromReaderIO[T]))
|
||||
}
|
||||
|
||||
// BindEitherK is a variant of Bind that works with Either (Result) computations.
|
||||
// It lifts an Either Kleisli arrow into the ReaderIOResult context.
|
||||
//
|
||||
// Parameters:
|
||||
// - setter: Updates state from S1 to S2 using result T
|
||||
// - f: An Either Kleisli arrow (S1 -> Either[error, T])
|
||||
//
|
||||
//go:inline
|
||||
func BindEitherK[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f result.Kleisli[S1, T],
|
||||
) Operator[S1, S2] {
|
||||
return Bind(setter, F.Flow2(f, FromEither[T]))
|
||||
}
|
||||
|
||||
// BindResultK is a variant of Bind that works with Result computations.
|
||||
// This is an alias for BindEitherK for consistency with the Result naming convention.
|
||||
//
|
||||
// Parameters:
|
||||
// - setter: Updates state from S1 to S2 using result T
|
||||
// - f: A Result Kleisli arrow (S1 -> Result[T])
|
||||
//
|
||||
//go:inline
|
||||
func BindResultK[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f result.Kleisli[S1, T],
|
||||
) Operator[S1, S2] {
|
||||
return Bind(setter, F.Flow2(f, FromResult[T]))
|
||||
}
|
||||
|
||||
// BindIOEitherKL is a lens-based variant of BindIOEitherK.
|
||||
// It combines a lens with an IOEither Kleisli arrow, focusing on a specific field
|
||||
// within the state structure.
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A lens focusing on field T within state S
|
||||
// - f: An IOEither Kleisli arrow (T -> IOEither[error, T])
|
||||
//
|
||||
//go:inline
|
||||
func BindIOEitherKL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f ioresult.Kleisli[T, T],
|
||||
) Operator[S, S] {
|
||||
return BindL(lens, F.Flow2(f, FromIOEither[T]))
|
||||
}
|
||||
|
||||
// BindIOResultKL is a lens-based variant of BindIOResultK.
|
||||
// This is an alias for BindIOEitherKL for consistency with the Result naming convention.
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A lens focusing on field T within state S
|
||||
// - f: An IOResult Kleisli arrow (T -> IOResult[T])
|
||||
//
|
||||
//go:inline
|
||||
func BindIOResultKL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f ioresult.Kleisli[T, T],
|
||||
) Operator[S, S] {
|
||||
return BindL(lens, F.Flow2(f, FromIOEither[T]))
|
||||
}
|
||||
|
||||
// BindIOKL is a lens-based variant of BindIOK.
|
||||
// It combines a lens with an IO Kleisli arrow, focusing on a specific field
|
||||
// within the state structure.
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A lens focusing on field T within state S
|
||||
// - f: An IO Kleisli arrow (T -> IO[T])
|
||||
//
|
||||
//go:inline
|
||||
func BindIOKL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f io.Kleisli[T, T],
|
||||
) Operator[S, S] {
|
||||
return BindL(lens, F.Flow2(f, FromIO[T]))
|
||||
}
|
||||
|
||||
// BindReaderKL is a lens-based variant of BindReaderK.
|
||||
// It combines a lens with a Reader Kleisli arrow (with context.Context), focusing on a specific field
|
||||
// within the state structure.
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A lens focusing on field T within state S
|
||||
// - f: A Reader Kleisli arrow (T -> Reader[context.Context, T])
|
||||
//
|
||||
//go:inline
|
||||
func BindReaderKL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f reader.Kleisli[context.Context, T, T],
|
||||
) Operator[S, S] {
|
||||
return BindL(lens, F.Flow2(f, FromReader[T]))
|
||||
}
|
||||
|
||||
// BindReaderIOKL is a lens-based variant of BindReaderIOK.
|
||||
// It combines a lens with a ReaderIO Kleisli arrow (with context.Context), focusing on a specific field
|
||||
// within the state structure.
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A lens focusing on field T within state S
|
||||
// - f: A ReaderIO Kleisli arrow (T -> ReaderIO[context.Context, T])
|
||||
//
|
||||
//go:inline
|
||||
func BindReaderIOKL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f readerio.Kleisli[context.Context, T, T],
|
||||
) Operator[S, S] {
|
||||
return BindL(lens, F.Flow2(f, FromReaderIO[T]))
|
||||
}
|
||||
|
||||
// ApIOEitherS is an applicative variant that works with IOEither values.
|
||||
// Unlike BindIOEitherK, this uses applicative composition (ApS) instead of monadic
|
||||
// composition (Bind), allowing independent computations to be combined.
|
||||
//
|
||||
// Parameters:
|
||||
// - setter: Updates state from S1 to S2 using result T
|
||||
// - fa: An IOEither value
|
||||
//
|
||||
//go:inline
|
||||
func ApIOEitherS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa IOResult[T],
|
||||
) Operator[S1, S2] {
|
||||
return F.Bind2nd(F.Flow2[ReaderIOResult[S1], ioresult.Operator[S1, S2]], ioeither.ApS(setter, fa))
|
||||
}
|
||||
|
||||
// ApIOResultS is an applicative variant that works with IOResult values.
|
||||
// This is an alias for ApIOEitherS for consistency with the Result naming convention.
|
||||
//
|
||||
// Parameters:
|
||||
// - setter: Updates state from S1 to S2 using result T
|
||||
// - fa: An IOResult value
|
||||
//
|
||||
//go:inline
|
||||
func ApIOResultS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa IOResult[T],
|
||||
) Operator[S1, S2] {
|
||||
return F.Bind2nd(F.Flow2[ReaderIOResult[S1], ioresult.Operator[S1, S2]], ioeither.ApS(setter, fa))
|
||||
}
|
||||
|
||||
// ApIOS is an applicative variant that works with IO values.
|
||||
// It lifts an IO value into the ReaderIOResult context using applicative composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - setter: Updates state from S1 to S2 using result T
|
||||
// - fa: An IO value
|
||||
//
|
||||
//go:inline
|
||||
func ApIOS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa IO[T],
|
||||
) Operator[S1, S2] {
|
||||
return ApS(setter, FromIO(fa))
|
||||
}
|
||||
|
||||
// ApReaderS is an applicative variant that works with Reader values.
|
||||
// It lifts a Reader value (with context.Context) into the ReaderIOResult context using applicative composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - setter: Updates state from S1 to S2 using result T
|
||||
// - fa: A Reader value
|
||||
//
|
||||
//go:inline
|
||||
func ApReaderS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Reader[context.Context, T],
|
||||
) Operator[S1, S2] {
|
||||
return ApS(setter, FromReader(fa))
|
||||
}
|
||||
|
||||
// ApReaderIOS is an applicative variant that works with ReaderIO values.
|
||||
// It lifts a ReaderIO value (with context.Context) into the ReaderIOResult context using applicative composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - setter: Updates state from S1 to S2 using result T
|
||||
// - fa: A ReaderIO value
|
||||
//
|
||||
//go:inline
|
||||
func ApReaderIOS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa ReaderIO[T],
|
||||
) Operator[S1, S2] {
|
||||
return ApS(setter, FromReaderIO(fa))
|
||||
}
|
||||
|
||||
// ApEitherS is an applicative variant that works with Either (Result) values.
|
||||
// It lifts an Either value into the ReaderIOResult context using applicative composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - setter: Updates state from S1 to S2 using result T
|
||||
// - fa: An Either value
|
||||
//
|
||||
//go:inline
|
||||
func ApEitherS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Result[T],
|
||||
) Operator[S1, S2] {
|
||||
return ApS(setter, FromEither(fa))
|
||||
}
|
||||
|
||||
// ApResultS is an applicative variant that works with Result values.
|
||||
// This is an alias for ApEitherS for consistency with the Result naming convention.
|
||||
//
|
||||
// Parameters:
|
||||
// - setter: Updates state from S1 to S2 using result T
|
||||
// - fa: A Result value
|
||||
//
|
||||
//go:inline
|
||||
func ApResultS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Result[T],
|
||||
) Operator[S1, S2] {
|
||||
return ApS(setter, FromResult(fa))
|
||||
}
|
||||
|
||||
// ApIOEitherSL is a lens-based variant of ApIOEitherS.
|
||||
// It combines a lens with an IOEither value using applicative composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A lens focusing on field T within state S
|
||||
// - fa: An IOEither value
|
||||
//
|
||||
//go:inline
|
||||
func ApIOEitherSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa IOResult[T],
|
||||
) Operator[S, S] {
|
||||
return F.Bind2nd(F.Flow2[ReaderIOResult[S], ioresult.Operator[S, S]], ioresult.ApSL(lens, fa))
|
||||
}
|
||||
|
||||
// ApIOResultSL is a lens-based variant of ApIOResultS.
|
||||
// This is an alias for ApIOEitherSL for consistency with the Result naming convention.
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A lens focusing on field T within state S
|
||||
// - fa: An IOResult value
|
||||
//
|
||||
//go:inline
|
||||
func ApIOResultSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa IOResult[T],
|
||||
) Operator[S, S] {
|
||||
return F.Bind2nd(F.Flow2[ReaderIOResult[S], ioresult.Operator[S, S]], ioresult.ApSL(lens, fa))
|
||||
}
|
||||
|
||||
// ApIOSL is a lens-based variant of ApIOS.
|
||||
// It combines a lens with an IO value using applicative composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A lens focusing on field T within state S
|
||||
// - fa: An IO value
|
||||
//
|
||||
//go:inline
|
||||
func ApIOSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa IO[T],
|
||||
) Operator[S, S] {
|
||||
return ApSL(lens, FromIO(fa))
|
||||
}
|
||||
|
||||
// ApReaderSL is a lens-based variant of ApReaderS.
|
||||
// It combines a lens with a Reader value (with context.Context) using applicative composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A lens focusing on field T within state S
|
||||
// - fa: A Reader value
|
||||
//
|
||||
//go:inline
|
||||
func ApReaderSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa Reader[context.Context, T],
|
||||
) Operator[S, S] {
|
||||
return ApSL(lens, FromReader(fa))
|
||||
}
|
||||
|
||||
// ApReaderIOSL is a lens-based variant of ApReaderIOS.
|
||||
// It combines a lens with a ReaderIO value (with context.Context) using applicative composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A lens focusing on field T within state S
|
||||
// - fa: A ReaderIO value
|
||||
//
|
||||
//go:inline
|
||||
func ApReaderIOSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa ReaderIO[T],
|
||||
) Operator[S, S] {
|
||||
return ApSL(lens, FromReaderIO(fa))
|
||||
}
|
||||
|
||||
// ApEitherSL is a lens-based variant of ApEitherS.
|
||||
// It combines a lens with an Either value using applicative composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A lens focusing on field T within state S
|
||||
// - fa: An Either value
|
||||
//
|
||||
//go:inline
|
||||
func ApEitherSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa Result[T],
|
||||
) Operator[S, S] {
|
||||
return ApSL(lens, FromEither(fa))
|
||||
}
|
||||
|
||||
// ApResultSL is a lens-based variant of ApResultS.
|
||||
// This is an alias for ApEitherSL for consistency with the Result naming convention.
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A lens focusing on field T within state S
|
||||
// - fa: A Result value
|
||||
//
|
||||
//go:inline
|
||||
func ApResultSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa Result[T],
|
||||
) Operator[S, S] {
|
||||
return ApSL(lens, FromResult(fa))
|
||||
}
|
||||
275
v2/context/readerioresult/bind_test.go
Normal file
275
v2/context/readerioresult/bind_test.go
Normal file
@@ -0,0 +1,275 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func getLastName(s utils.Initial) ReaderIOResult[string] {
|
||||
return Of("Doe")
|
||||
}
|
||||
|
||||
func getGivenName(s utils.WithLastName) ReaderIOResult[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),
|
||||
)
|
||||
|
||||
assert.Equal(t, res(t.Context())(), E.Of[error]("John Doe"))
|
||||
}
|
||||
|
||||
func TestApS(t *testing.T) {
|
||||
|
||||
res := F.Pipe3(
|
||||
Do(utils.Empty),
|
||||
ApS(utils.SetLastName, Of("Doe")),
|
||||
ApS(utils.SetGivenName, Of("John")),
|
||||
Map(utils.GetFullName),
|
||||
)
|
||||
|
||||
assert.Equal(t, res(t.Context())(), E.Of[error]("John Doe"))
|
||||
}
|
||||
|
||||
func TestApS_WithError(t *testing.T) {
|
||||
// Test that ApS propagates errors correctly
|
||||
testErr := assert.AnError
|
||||
|
||||
res := F.Pipe3(
|
||||
Do(utils.Empty),
|
||||
ApS(utils.SetLastName, Left[string](testErr)),
|
||||
ApS(utils.SetGivenName, Of("John")),
|
||||
Map(utils.GetFullName),
|
||||
)
|
||||
|
||||
result := res(t.Context())()
|
||||
assert.True(t, E.IsLeft(result))
|
||||
assert.Equal(t, testErr, E.ToError(result))
|
||||
}
|
||||
|
||||
func TestApS_WithSecondError(t *testing.T) {
|
||||
// Test that ApS propagates errors from the second operation
|
||||
testErr := assert.AnError
|
||||
|
||||
res := F.Pipe3(
|
||||
Do(utils.Empty),
|
||||
ApS(utils.SetLastName, Of("Doe")),
|
||||
ApS(utils.SetGivenName, Left[string](testErr)),
|
||||
Map(utils.GetFullName),
|
||||
)
|
||||
|
||||
result := res(t.Context())()
|
||||
assert.True(t, E.IsLeft(result))
|
||||
assert.Equal(t, testErr, E.ToError(result))
|
||||
}
|
||||
|
||||
func TestApS_MultipleFields(t *testing.T) {
|
||||
// Test ApS with more than two fields
|
||||
type Person struct {
|
||||
FirstName string
|
||||
MiddleName string
|
||||
LastName string
|
||||
Age int
|
||||
}
|
||||
|
||||
setFirstName := func(s string) func(Person) Person {
|
||||
return func(p Person) Person {
|
||||
p.FirstName = s
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
setMiddleName := func(s string) func(Person) Person {
|
||||
return func(p Person) Person {
|
||||
p.MiddleName = s
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
setLastName := func(s string) func(Person) Person {
|
||||
return func(p Person) Person {
|
||||
p.LastName = s
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
setAge := func(a int) func(Person) Person {
|
||||
return func(p Person) Person {
|
||||
p.Age = a
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
res := F.Pipe5(
|
||||
Do(Person{}),
|
||||
ApS(setFirstName, Of("John")),
|
||||
ApS(setMiddleName, Of("Q")),
|
||||
ApS(setLastName, Of("Doe")),
|
||||
ApS(setAge, Of(42)),
|
||||
Map(func(p Person) Person { return p }),
|
||||
)
|
||||
|
||||
result := res(t.Context())()
|
||||
assert.True(t, E.IsRight(result))
|
||||
person := E.ToOption(result)
|
||||
assert.True(t, O.IsSome(person))
|
||||
p, _ := O.Unwrap(person)
|
||||
assert.Equal(t, "John", p.FirstName)
|
||||
assert.Equal(t, "Q", p.MiddleName)
|
||||
assert.Equal(t, "Doe", p.LastName)
|
||||
assert.Equal(t, 42, p.Age)
|
||||
}
|
||||
|
||||
func TestApS_WithDifferentTypes(t *testing.T) {
|
||||
// Test ApS with different value types
|
||||
type State struct {
|
||||
Name string
|
||||
Count int
|
||||
Flag bool
|
||||
}
|
||||
|
||||
setName := func(s string) func(State) State {
|
||||
return func(st State) State {
|
||||
st.Name = s
|
||||
return st
|
||||
}
|
||||
}
|
||||
|
||||
setCount := func(c int) func(State) State {
|
||||
return func(st State) State {
|
||||
st.Count = c
|
||||
return st
|
||||
}
|
||||
}
|
||||
|
||||
setFlag := func(f bool) func(State) State {
|
||||
return func(st State) State {
|
||||
st.Flag = f
|
||||
return st
|
||||
}
|
||||
}
|
||||
|
||||
res := F.Pipe4(
|
||||
Do(State{}),
|
||||
ApS(setName, Of("test")),
|
||||
ApS(setCount, Of(100)),
|
||||
ApS(setFlag, Of(true)),
|
||||
Map(func(s State) State { return s }),
|
||||
)
|
||||
|
||||
result := res(t.Context())()
|
||||
assert.True(t, E.IsRight(result))
|
||||
stateOpt := E.ToOption(result)
|
||||
assert.True(t, O.IsSome(stateOpt))
|
||||
state, _ := O.Unwrap(stateOpt)
|
||||
assert.Equal(t, "test", state.Name)
|
||||
assert.Equal(t, 100, state.Count)
|
||||
assert.True(t, state.Flag)
|
||||
}
|
||||
|
||||
func TestApS_EmptyState(t *testing.T) {
|
||||
// Test ApS starting with an empty state
|
||||
type Empty struct{}
|
||||
|
||||
res := Do(Empty{})
|
||||
|
||||
result := res(t.Context())()
|
||||
assert.True(t, E.IsRight(result))
|
||||
emptyOpt := E.ToOption(result)
|
||||
assert.True(t, O.IsSome(emptyOpt))
|
||||
empty, _ := O.Unwrap(emptyOpt)
|
||||
assert.Equal(t, Empty{}, empty)
|
||||
}
|
||||
|
||||
func TestApS_ChainedWithBind(t *testing.T) {
|
||||
// Test mixing ApS with Bind operations
|
||||
type State struct {
|
||||
Independent string
|
||||
Dependent string
|
||||
}
|
||||
|
||||
setIndependent := func(s string) func(State) State {
|
||||
return func(st State) State {
|
||||
st.Independent = s
|
||||
return st
|
||||
}
|
||||
}
|
||||
|
||||
setDependent := func(s string) func(State) State {
|
||||
return func(st State) State {
|
||||
st.Dependent = s
|
||||
return st
|
||||
}
|
||||
}
|
||||
|
||||
getDependentValue := func(s State) ReaderIOResult[string] {
|
||||
// This depends on the Independent field
|
||||
return Of(s.Independent + "-dependent")
|
||||
}
|
||||
|
||||
res := F.Pipe3(
|
||||
Do(State{}),
|
||||
ApS(setIndependent, Of("value")),
|
||||
Bind(setDependent, getDependentValue),
|
||||
Map(func(s State) State { return s }),
|
||||
)
|
||||
|
||||
result := res(t.Context())()
|
||||
assert.True(t, E.IsRight(result))
|
||||
stateOpt := E.ToOption(result)
|
||||
assert.True(t, O.IsSome(stateOpt))
|
||||
state, _ := O.Unwrap(stateOpt)
|
||||
assert.Equal(t, "value", state.Independent)
|
||||
assert.Equal(t, "value-dependent", state.Dependent)
|
||||
}
|
||||
|
||||
func TestApS_WithContextCancellation(t *testing.T) {
|
||||
// Test that ApS respects context cancellation
|
||||
type State struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
setValue := func(s string) func(State) State {
|
||||
return func(st State) State {
|
||||
st.Value = s
|
||||
return st
|
||||
}
|
||||
}
|
||||
|
||||
// Create a computation that would succeed
|
||||
computation := ApS(setValue, Of("test"))(Do(State{}))
|
||||
|
||||
// Create a cancelled context
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
result := computation(ctx)()
|
||||
assert.True(t, E.IsLeft(result))
|
||||
}
|
||||
@@ -13,13 +13,10 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioeither
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/internal/bracket"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||
)
|
||||
|
||||
// Bracket makes sure that a resource is cleaned up in the event of an error. The release action is called regardless of
|
||||
@@ -27,18 +24,9 @@ import (
|
||||
func Bracket[
|
||||
A, B, ANY any](
|
||||
|
||||
acquire ReaderIOEither[A],
|
||||
use func(A) ReaderIOEither[B],
|
||||
release func(A, Either[B]) ReaderIOEither[ANY],
|
||||
) ReaderIOEither[B] {
|
||||
return bracket.Bracket[ReaderIOEither[A], ReaderIOEither[B], ReaderIOEither[ANY], Either[B], A, B](
|
||||
readerio.Of[context.Context, Either[B]],
|
||||
MonadChain[A, B],
|
||||
readerio.MonadChain[context.Context, Either[B], Either[B]],
|
||||
MonadChain[ANY, B],
|
||||
|
||||
acquire,
|
||||
use,
|
||||
release,
|
||||
)
|
||||
acquire ReaderIOResult[A],
|
||||
use Kleisli[A, B],
|
||||
release func(A, Either[B]) ReaderIOResult[ANY],
|
||||
) ReaderIOResult[B] {
|
||||
return RIOR.Bracket(acquire, use, release)
|
||||
}
|
||||
@@ -13,26 +13,26 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioeither
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
CIOE "github.com/IBM/fp-go/v2/context/ioeither"
|
||||
CIOE "github.com/IBM/fp-go/v2/context/ioresult"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
)
|
||||
|
||||
// WithContext wraps an existing [ReaderIOEither] and performs a context check for cancellation before delegating.
|
||||
// WithContext wraps an existing [ReaderIOResult] and performs a context check for cancellation before delegating.
|
||||
// This ensures that if the context is already canceled, the computation short-circuits immediately
|
||||
// without executing the wrapped computation.
|
||||
//
|
||||
// This is useful for adding cancellation awareness to computations that might not check the context themselves.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The ReaderIOEither to wrap with context checking
|
||||
// - ma: The ReaderIOResult to wrap with context checking
|
||||
//
|
||||
// Returns a ReaderIOEither that checks for cancellation before executing.
|
||||
func WithContext[A any](ma ReaderIOEither[A]) ReaderIOEither[A] {
|
||||
// Returns a ReaderIOResult that checks for cancellation before executing.
|
||||
func WithContext[A any](ma ReaderIOResult[A]) ReaderIOResult[A] {
|
||||
return func(ctx context.Context) IOEither[A] {
|
||||
if err := context.Cause(ctx); err != nil {
|
||||
return ioeither.Left[A](err)
|
||||
251
v2/context/readerioresult/coverage.out
Normal file
251
v2/context/readerioresult/coverage.out
Normal file
@@ -0,0 +1,251 @@
|
||||
mode: set
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/bind.go:27.21,29.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/bind.go:35.47,42.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/bind.go:48.47,54.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/bind.go:60.47,66.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/bind.go:71.46,76.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/bind.go:82.47,89.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/bracket.go:33.21,44.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/cancel.go:35.65,36.47 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/cancel.go:36.47,37.44 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/cancel.go:37.44,39.4 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/cancel.go:40.3,40.40 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/eq.go:42.84,44.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:18.91,20.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:24.93,26.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:30.101,32.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:36.103,38.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:43.36,48.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:53.36,58.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:63.36,68.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:71.98,76.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:79.101,84.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:87.101,92.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:95.129,96.68 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:96.68,102.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:106.132,107.68 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:107.68,113.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:117.132,118.68 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:118.68,124.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:129.113,131.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:135.115,137.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:143.40,150.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:156.40,163.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:169.40,176.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:179.126,185.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:188.129,194.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:197.129,203.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:206.185,207.76 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:207.76,215.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:219.188,220.76 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:220.76,228.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:232.188,233.76 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:233.76,241.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:246.125,248.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:252.127,254.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:261.44,270.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:277.44,286.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:293.44,302.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:305.154,312.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:315.157,322.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:325.157,332.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:335.241,336.84 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:336.84,346.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:350.244,351.84 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:351.84,361.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:365.244,366.84 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:366.84,376.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:381.137,383.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:387.139,389.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:397.48,408.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:416.48,427.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:435.48,446.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:449.182,457.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:460.185,468.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:471.185,479.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:482.297,483.92 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:483.92,495.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:499.300,500.92 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:500.92,512.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:516.300,517.92 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:517.92,529.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:534.149,536.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:540.151,542.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:551.52,564.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:573.52,586.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:595.52,608.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:611.210,620.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:623.213,632.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:635.213,644.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:647.353,648.100 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:648.100,662.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:666.356,667.100 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:667.100,681.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:685.356,686.100 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:686.100,700.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:705.161,707.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:711.163,713.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:723.56,738.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:748.56,763.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:773.56,788.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:791.238,801.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:804.241,814.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:817.241,827.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:830.409,831.108 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:831.108,847.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:851.412,852.108 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:852.108,868.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:872.412,873.108 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:873.108,889.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:894.173,896.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:900.175,902.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:913.60,930.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:941.60,958.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:969.60,986.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:989.266,1000.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1003.269,1014.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1017.269,1028.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1031.465,1032.116 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1032.116,1050.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1054.468,1055.116 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1055.116,1073.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1077.468,1078.116 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1078.116,1096.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1101.185,1103.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1107.187,1109.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1121.64,1140.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1152.64,1171.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1183.64,1202.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1205.294,1217.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1220.297,1232.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1235.297,1247.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1250.521,1251.124 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1251.124,1271.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1275.524,1276.124 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1276.124,1296.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1300.524,1301.124 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1301.124,1321.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1326.197,1328.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1332.199,1334.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1347.68,1368.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1381.68,1402.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1415.68,1436.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1439.322,1452.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1455.325,1468.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1471.325,1484.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1487.577,1488.132 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1488.132,1510.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1514.580,1515.132 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1515.132,1537.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1541.580,1542.132 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1542.132,1564.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1569.210,1571.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1575.212,1577.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1591.74,1614.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1628.74,1651.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1665.74,1688.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1691.356,1705.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1708.359,1722.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1725.359,1739.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1742.645,1743.144 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1743.144,1767.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1771.648,1772.144 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1772.144,1796.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1800.648,1801.144 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1801.144,1825.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/monoid.go:36.61,43.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/monoid.go:52.64,59.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/monoid.go:68.64,75.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/monoid.go:85.61,93.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/monoid.go:103.63,108.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:42.55,44.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:52.45,54.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:62.42,64.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:74.78,76.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:85.75,87.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:97.72,99.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:108.69,110.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:120.96,122.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:131.93,133.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:143.101,145.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:154.71,156.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:165.39,167.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:169.93,173.56 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:173.56,174.32 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:174.32,174.47 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:189.98,194.47 3 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:194.47,196.44 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:196.44,198.4 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:200.3,200.27 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:200.27,202.45 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:202.45,204.5 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:207.4,213.47 5 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:227.95,229.17 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:229.17,231.3 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:232.2,232.28 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:243.98,245.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:254.91,256.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:265.94,267.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:276.94,278.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:288.95,290.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:299.73,301.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:307.44,309.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:319.95,321.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:330.95,332.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:342.100,344.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:353.100,355.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:364.116,366.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:375.75,377.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:386.47,388.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:398.51,400.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:406.39,407.47 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:407.47,408.27 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:408.27,411.4 2 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:423.87,425.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:434.87,436.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:446.92,448.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:457.92,459.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:468.115,470.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:479.85,480.54 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:480.54,481.48 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:481.48,482.28 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:482.28,487.12 3 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:488.30,489.22 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:490.23,491.47 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:505.59,511.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:520.66,522.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:531.83,533.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:543.97,545.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:554.64,556.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:566.62,568.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:577.78,579.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:589.80,591.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:600.76,602.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:612.136,614.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:623.91,625.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:634.71,636.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/resource.go:58.151,63.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/semigroup.go:39.41,43.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/sync.go:46.78,54.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:31.89,39.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:48.103,56.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:65.71,67.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:75.112,83.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:92.124,100.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:108.94,110.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:120.95,128.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:137.92,145.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:148.106,156.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:165.74,167.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:170.118,178.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:181.115,189.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:192.127,200.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:203.97,205.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:215.95,223.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:232.92,240.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:243.106,251.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:260.74,262.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:265.115,273.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:276.127,284.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:287.118,295.2 1 0
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:304.97,306.2 1 0
|
||||
@@ -13,13 +13,13 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package readerioeither provides a specialized version of [readerioeither.ReaderIOEither] that uses
|
||||
// package readerioresult provides a specialized version of [readerioeither.ReaderIOResult] that uses
|
||||
// [context.Context] as the context type and [error] as the left (error) type. This package is designed
|
||||
// for typical Go applications where context-aware, effectful computations with error handling are needed.
|
||||
//
|
||||
// # Core Concept
|
||||
//
|
||||
// ReaderIOEither[A] represents a computation that:
|
||||
// ReaderIOResult[A] represents a computation that:
|
||||
// - Depends on a [context.Context] (Reader aspect)
|
||||
// - Performs side effects (IO aspect)
|
||||
// - Can fail with an [error] (Either aspect)
|
||||
@@ -27,7 +27,7 @@
|
||||
//
|
||||
// The type is defined as:
|
||||
//
|
||||
// ReaderIOEither[A] = func(context.Context) func() Either[error, A]
|
||||
// ReaderIOResult[A] = func(context.Context) func() Either[error, A]
|
||||
//
|
||||
// This combines three powerful functional programming concepts:
|
||||
// - Reader: Dependency injection via context
|
||||
@@ -50,7 +50,7 @@
|
||||
// - [Left]: Create failed computations
|
||||
// - [FromEither], [FromIO], [FromIOEither]: Convert from other types
|
||||
// - [TryCatch]: Wrap error-returning functions
|
||||
// - [Eitherize0-10]: Convert standard Go functions to ReaderIOEither
|
||||
// - [Eitherize0-10]: Convert standard Go functions to ReaderIOResult
|
||||
//
|
||||
// Transformation:
|
||||
// - [Map]: Transform success values
|
||||
@@ -90,15 +90,15 @@
|
||||
// import (
|
||||
// "context"
|
||||
// "fmt"
|
||||
// RIOE "github.com/IBM/fp-go/v2/context/readerioeither"
|
||||
// RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// )
|
||||
//
|
||||
// // Define a computation that reads from context and may fail
|
||||
// func fetchUser(id string) RIOE.ReaderIOEither[User] {
|
||||
// func fetchUser(id string) RIOE.ReaderIOResult[User] {
|
||||
// return F.Pipe2(
|
||||
// RIOE.Ask(),
|
||||
// RIOE.Chain(func(ctx context.Context) RIOE.ReaderIOEither[User] {
|
||||
// RIOE.Chain(func(ctx context.Context) RIOE.ReaderIOResult[User] {
|
||||
// return RIOE.TryCatch(func(ctx context.Context) func() (User, error) {
|
||||
// return func() (User, error) {
|
||||
// return userService.Get(ctx, id)
|
||||
@@ -138,8 +138,8 @@
|
||||
// openFile("data.txt"),
|
||||
// closeFile,
|
||||
// ),
|
||||
// func(use func(func(*os.File) RIOE.ReaderIOEither[string]) RIOE.ReaderIOEither[string]) RIOE.ReaderIOEither[string] {
|
||||
// return use(func(file *os.File) RIOE.ReaderIOEither[string] {
|
||||
// func(use func(func(*os.File) RIOE.ReaderIOResult[string]) RIOE.ReaderIOResult[string]) RIOE.ReaderIOResult[string] {
|
||||
// return use(func(file *os.File) RIOE.ReaderIOResult[string] {
|
||||
// return readContent(file)
|
||||
// })
|
||||
// },
|
||||
@@ -166,4 +166,4 @@
|
||||
// result := computation(ctx)() // Returns Left with cancellation error
|
||||
//
|
||||
//go:generate go run ../.. contextreaderioeither --count 10 --filename gen.go
|
||||
package readerioeither
|
||||
package readerioresult
|
||||
@@ -13,7 +13,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioeither
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -22,14 +22,14 @@ import (
|
||||
RIOE "github.com/IBM/fp-go/v2/readerioeither"
|
||||
)
|
||||
|
||||
// Eq implements the equals predicate for values contained in the [ReaderIOEither] monad.
|
||||
// It creates an equality checker that can compare two ReaderIOEither values by executing them
|
||||
// Eq implements the equals predicate for values contained in the [ReaderIOResult] monad.
|
||||
// It creates an equality checker that can compare two ReaderIOResult values by executing them
|
||||
// with a given context and comparing their results using the provided Either equality checker.
|
||||
//
|
||||
// Parameters:
|
||||
// - eq: Equality checker for Either[A] values
|
||||
//
|
||||
// Returns a function that takes a context and returns an equality checker for ReaderIOEither[A].
|
||||
// Returns a function that takes a context and returns an equality checker for ReaderIOResult[A].
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
@@ -39,6 +39,8 @@ import (
|
||||
// eqRIE := Eq(eqInt)
|
||||
// ctx := context.Background()
|
||||
// equal := eqRIE(ctx).Equals(Right[int](42), Right[int](42)) // true
|
||||
func Eq[A any](eq eq.Eq[Either[A]]) func(context.Context) eq.Eq[ReaderIOEither[A]] {
|
||||
//
|
||||
//go:inline
|
||||
func Eq[A any](eq eq.Eq[Either[A]]) func(context.Context) eq.Eq[ReaderIOResult[A]] {
|
||||
return RIOE.Eq[context.Context](eq)
|
||||
}
|
||||
@@ -18,7 +18,7 @@ package exec
|
||||
import (
|
||||
"context"
|
||||
|
||||
RIOE "github.com/IBM/fp-go/v2/context/readerioeither"
|
||||
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/exec"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
GE "github.com/IBM/fp-go/v2/internal/exec"
|
||||
@@ -30,7 +30,7 @@ var (
|
||||
Command = F.Curry3(command)
|
||||
)
|
||||
|
||||
func command(name string, args []string, in []byte) RIOE.ReaderIOEither[exec.CommandOutput] {
|
||||
func command(name string, args []string, in []byte) RIOE.ReaderIOResult[exec.CommandOutput] {
|
||||
return func(ctx context.Context) IOE.IOEither[error, exec.CommandOutput] {
|
||||
return IOE.TryCatchError(func() (exec.CommandOutput, error) {
|
||||
return GE.Exec(ctx, name, args, in)
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
RIOE "github.com/IBM/fp-go/v2/context/readerioeither"
|
||||
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
ET "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/file"
|
||||
@@ -44,7 +44,7 @@ var (
|
||||
)
|
||||
|
||||
// Close closes an object
|
||||
func Close[C io.Closer](c C) RIOE.ReaderIOEither[any] {
|
||||
func Close[C io.Closer](c C) RIOE.ReaderIOResult[any] {
|
||||
return F.Pipe2(
|
||||
c,
|
||||
IOEF.Close[C],
|
||||
@@ -53,8 +53,8 @@ func Close[C io.Closer](c C) RIOE.ReaderIOEither[any] {
|
||||
}
|
||||
|
||||
// ReadFile reads a file in the scope of a context
|
||||
func ReadFile(path string) RIOE.ReaderIOEither[[]byte] {
|
||||
return RIOE.WithResource[[]byte](Open(path), Close[*os.File])(func(r *os.File) RIOE.ReaderIOEither[[]byte] {
|
||||
func ReadFile(path string) RIOE.ReaderIOResult[[]byte] {
|
||||
return RIOE.WithResource[[]byte](Open(path), Close[*os.File])(func(r *os.File) RIOE.ReaderIOResult[[]byte] {
|
||||
return func(ctx context.Context) IOE.IOEither[error, []byte] {
|
||||
return func() ET.Either[error, []byte] {
|
||||
return file.ReadAll(ctx, r)
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
R "github.com/IBM/fp-go/v2/context/readerioeither"
|
||||
R "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
J "github.com/IBM/fp-go/v2/json"
|
||||
@@ -18,7 +18,7 @@ package file
|
||||
import (
|
||||
"os"
|
||||
|
||||
RIOE "github.com/IBM/fp-go/v2/context/readerioeither"
|
||||
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
IO "github.com/IBM/fp-go/v2/io"
|
||||
IOF "github.com/IBM/fp-go/v2/io/file"
|
||||
@@ -38,7 +38,7 @@ var (
|
||||
)
|
||||
|
||||
// CreateTemp created a temp file with proper parametrization
|
||||
func CreateTemp(dir, pattern string) RIOE.ReaderIOEither[*os.File] {
|
||||
func CreateTemp(dir, pattern string) RIOE.ReaderIOResult[*os.File] {
|
||||
return F.Pipe2(
|
||||
IOEF.CreateTemp(dir, pattern),
|
||||
RIOE.FromIOEither[*os.File],
|
||||
@@ -47,6 +47,6 @@ func CreateTemp(dir, pattern string) RIOE.ReaderIOEither[*os.File] {
|
||||
}
|
||||
|
||||
// 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 func(*os.File) RIOE.ReaderIOEither[A]) RIOE.ReaderIOEither[A] {
|
||||
func WithTempFile[A any](f func(*os.File) RIOE.ReaderIOResult[A]) RIOE.ReaderIOResult[A] {
|
||||
return RIOE.WithResource[A](onCreateTempFile, onReleaseTempFile)(f)
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
RIOE "github.com/IBM/fp-go/v2/context/readerioeither"
|
||||
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -35,7 +35,7 @@ func TestWithTempFile(t *testing.T) {
|
||||
|
||||
func TestWithTempFileOnClosedFile(t *testing.T) {
|
||||
|
||||
res := WithTempFile(func(f *os.File) RIOE.ReaderIOEither[[]byte] {
|
||||
res := WithTempFile(func(f *os.File) RIOE.ReaderIOResult[[]byte] {
|
||||
return F.Pipe2(
|
||||
f,
|
||||
onWriteAll[*os.File]([]byte("Carsten")),
|
||||
@@ -19,12 +19,12 @@ import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
RIOE "github.com/IBM/fp-go/v2/context/readerioeither"
|
||||
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
func onWriteAll[W io.Writer](data []byte) func(w W) RIOE.ReaderIOEither[[]byte] {
|
||||
return func(w W) RIOE.ReaderIOEither[[]byte] {
|
||||
func onWriteAll[W io.Writer](data []byte) func(w W) RIOE.ReaderIOResult[[]byte] {
|
||||
return func(w W) RIOE.ReaderIOResult[[]byte] {
|
||||
return F.Pipe1(
|
||||
RIOE.TryCatch(func(_ context.Context) func() ([]byte, error) {
|
||||
return func() ([]byte, error) {
|
||||
@@ -38,9 +38,9 @@ func onWriteAll[W io.Writer](data []byte) func(w W) RIOE.ReaderIOEither[[]byte]
|
||||
}
|
||||
|
||||
// WriteAll uses a generator function to create a stream, writes data to it and closes it
|
||||
func WriteAll[W io.WriteCloser](data []byte) func(acquire RIOE.ReaderIOEither[W]) RIOE.ReaderIOEither[[]byte] {
|
||||
func WriteAll[W io.WriteCloser](data []byte) func(acquire RIOE.ReaderIOResult[W]) RIOE.ReaderIOResult[[]byte] {
|
||||
onWrite := onWriteAll[W](data)
|
||||
return func(onCreate RIOE.ReaderIOEither[W]) RIOE.ReaderIOEither[[]byte] {
|
||||
return func(onCreate RIOE.ReaderIOResult[W]) RIOE.ReaderIOResult[[]byte] {
|
||||
return RIOE.WithResource[[]byte](
|
||||
onCreate,
|
||||
Close[W])(
|
||||
@@ -50,7 +50,7 @@ func WriteAll[W io.WriteCloser](data []byte) func(acquire RIOE.ReaderIOEither[W]
|
||||
}
|
||||
|
||||
// Write uses a generator function to create a stream, writes data to it and closes it
|
||||
func Write[R any, W io.WriteCloser](acquire RIOE.ReaderIOEither[W]) func(use func(W) RIOE.ReaderIOEither[R]) RIOE.ReaderIOEither[R] {
|
||||
func Write[R any, W io.WriteCloser](acquire RIOE.ReaderIOResult[W]) func(use func(W) RIOE.ReaderIOResult[R]) RIOE.ReaderIOResult[R] {
|
||||
return RIOE.WithResource[R](
|
||||
acquire,
|
||||
Close[W])
|
||||
295
v2/context/readerioresult/flip.go
Normal file
295
v2/context/readerioresult/flip.go
Normal file
@@ -0,0 +1,295 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
RIO "github.com/IBM/fp-go/v2/readerio"
|
||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||
RR "github.com/IBM/fp-go/v2/readerresult"
|
||||
)
|
||||
|
||||
// SequenceReader transforms a ReaderIOResult containing a Reader into a function that
|
||||
// takes the Reader's environment first, then returns a ReaderIOResult.
|
||||
//
|
||||
// This function "flips" or "sequences" the nested structure, changing the order in which
|
||||
// parameters are applied. It's particularly useful for point-free style programming where
|
||||
// you want to partially apply the inner Reader's environment before dealing with the
|
||||
// outer context.
|
||||
//
|
||||
// Type transformation:
|
||||
//
|
||||
// From: ReaderIOResult[Reader[R, A]]
|
||||
// = func(context.Context) func() Either[error, func(R) A]
|
||||
//
|
||||
// To: func(context.Context) func(R) IOResult[A]
|
||||
// = func(context.Context) func(R) func() Either[error, A]
|
||||
//
|
||||
// This allows you to:
|
||||
// 1. Provide the context.Context first
|
||||
// 2. Then provide the Reader's environment R
|
||||
// 3. Finally execute the IO effect to get Either[error, A]
|
||||
//
|
||||
// Point-free style benefits:
|
||||
// - Enables partial application of the Reader environment
|
||||
// - Facilitates composition of Reader-based computations
|
||||
// - Allows building reusable computation pipelines
|
||||
// - Supports dependency injection patterns where R represents dependencies
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Timeout int
|
||||
// }
|
||||
//
|
||||
// // A computation that produces a Reader based on context
|
||||
// func getMultiplier(ctx context.Context) func() Either[error, func(Config) int] {
|
||||
// return func() Either[error, func(Config) int] {
|
||||
// return Right[error](func(cfg Config) int {
|
||||
// return cfg.Timeout * 2
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Sequence it to apply Config first
|
||||
// sequenced := SequenceReader[Config, int](getMultiplier)
|
||||
//
|
||||
// // Now we can partially apply the Config
|
||||
// cfg := Config{Timeout: 30}
|
||||
// ctx := context.Background()
|
||||
// result := sequenced(ctx)(cfg)() // Returns Right(60)
|
||||
//
|
||||
// This is especially useful in point-free style when building computation pipelines:
|
||||
//
|
||||
// var pipeline = F.Flow3(
|
||||
// loadConfig, // ReaderIOResult[Reader[Database, Config]]
|
||||
// SequenceReader, // func(context.Context) func(Database) IOResult[Config]
|
||||
// applyToDatabase(db), // IOResult[Config]
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func SequenceReader[R, A any](ma ReaderIOResult[Reader[R, A]]) reader.Kleisli[context.Context, R, IOResult[A]] {
|
||||
return RIOR.SequenceReader(ma)
|
||||
}
|
||||
|
||||
// SequenceReaderIO transforms a ReaderIOResult containing a ReaderIO into a function that
|
||||
// takes the ReaderIO's environment first, then returns a ReaderIOResult.
|
||||
//
|
||||
// This is similar to SequenceReader but works with ReaderIO, which represents a computation
|
||||
// that depends on an environment R and performs IO effects.
|
||||
//
|
||||
// Type transformation:
|
||||
//
|
||||
// From: ReaderIOResult[ReaderIO[R, A]]
|
||||
// = func(context.Context) func() Either[error, func(R) func() A]
|
||||
//
|
||||
// To: func(context.Context) func(R) IOResult[A]
|
||||
// = func(context.Context) func(R) func() Either[error, A]
|
||||
//
|
||||
// The key difference from SequenceReader is that the inner computation (ReaderIO) already
|
||||
// performs IO effects, so the sequencing combines these effects properly.
|
||||
//
|
||||
// Point-free style benefits:
|
||||
// - Enables composition of ReaderIO-based computations
|
||||
// - Allows partial application of environment before IO execution
|
||||
// - Facilitates building effect pipelines with dependency injection
|
||||
// - Supports layered architecture where R represents service dependencies
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Database struct {
|
||||
// ConnectionString string
|
||||
// }
|
||||
//
|
||||
// // A computation that produces a ReaderIO based on context
|
||||
// func getQuery(ctx context.Context) func() Either[error, func(Database) func() string] {
|
||||
// return func() Either[error, func(Database) func() string] {
|
||||
// return Right[error](func(db Database) func() string {
|
||||
// return func() string {
|
||||
// // Perform actual IO here
|
||||
// return "Query result from " + db.ConnectionString
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Sequence it to apply Database first
|
||||
// sequenced := SequenceReaderIO[Database, string](getQuery)
|
||||
//
|
||||
// // Partially apply the Database
|
||||
// db := Database{ConnectionString: "localhost:5432"}
|
||||
// ctx := context.Background()
|
||||
// result := sequenced(ctx)(db)() // Executes IO and returns Right("Query result...")
|
||||
//
|
||||
// In point-free style, this enables clean composition:
|
||||
//
|
||||
// var executeQuery = F.Flow3(
|
||||
// prepareQuery, // ReaderIOResult[ReaderIO[Database, QueryResult]]
|
||||
// SequenceReaderIO, // func(context.Context) func(Database) IOResult[QueryResult]
|
||||
// withDatabase(db), // IOResult[QueryResult]
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func SequenceReaderIO[R, A any](ma ReaderIOResult[RIO.ReaderIO[R, A]]) reader.Kleisli[context.Context, R, IOResult[A]] {
|
||||
return RIOR.SequenceReaderIO(ma)
|
||||
}
|
||||
|
||||
// SequenceReaderResult transforms a ReaderIOResult containing a ReaderResult into a function
|
||||
// that takes the ReaderResult's environment first, then returns a ReaderIOResult.
|
||||
//
|
||||
// This is similar to SequenceReader but works with ReaderResult, which represents a computation
|
||||
// that depends on an environment R and can fail with an error.
|
||||
//
|
||||
// Type transformation:
|
||||
//
|
||||
// From: ReaderIOResult[ReaderResult[R, A]]
|
||||
// = func(context.Context) func() Either[error, func(R) Either[error, A]]
|
||||
//
|
||||
// To: func(context.Context) func(R) IOResult[A]
|
||||
// = func(context.Context) func(R) func() Either[error, A]
|
||||
//
|
||||
// The sequencing properly combines the error handling from both the outer ReaderIOResult
|
||||
// and the inner ReaderResult, ensuring that errors from either level are propagated correctly.
|
||||
//
|
||||
// Point-free style benefits:
|
||||
// - Enables composition of error-handling computations with dependency injection
|
||||
// - Allows partial application of dependencies before error handling
|
||||
// - Facilitates building validation pipelines with environment dependencies
|
||||
// - Supports service-oriented architectures with proper error propagation
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// MaxRetries int
|
||||
// }
|
||||
//
|
||||
// // A computation that produces a ReaderResult based on context
|
||||
// func validateRetries(ctx context.Context) func() Either[error, func(Config) Either[error, int]] {
|
||||
// return func() Either[error, func(Config) Either[error, int]] {
|
||||
// return Right[error](func(cfg Config) Either[error, int] {
|
||||
// if cfg.MaxRetries < 0 {
|
||||
// return Left[int](errors.New("negative retries"))
|
||||
// }
|
||||
// return Right[error](cfg.MaxRetries)
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Sequence it to apply Config first
|
||||
// sequenced := SequenceReaderResult[Config, int](validateRetries)
|
||||
//
|
||||
// // Partially apply the Config
|
||||
// cfg := Config{MaxRetries: 3}
|
||||
// ctx := context.Background()
|
||||
// result := sequenced(ctx)(cfg)() // Returns Right(3)
|
||||
//
|
||||
// // With invalid config
|
||||
// badCfg := Config{MaxRetries: -1}
|
||||
// badResult := sequenced(ctx)(badCfg)() // Returns Left(error("negative retries"))
|
||||
//
|
||||
// In point-free style, this enables validation pipelines:
|
||||
//
|
||||
// var validateAndProcess = F.Flow4(
|
||||
// loadConfig, // ReaderIOResult[ReaderResult[Config, Settings]]
|
||||
// SequenceReaderResult, // func(context.Context) func(Config) IOResult[Settings]
|
||||
// applyConfig(cfg), // IOResult[Settings]
|
||||
// Chain(processSettings), // IOResult[Result]
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func SequenceReaderResult[R, A any](ma ReaderIOResult[RR.ReaderResult[R, A]]) reader.Kleisli[context.Context, R, IOResult[A]] {
|
||||
return RIOR.SequenceReaderEither(ma)
|
||||
}
|
||||
|
||||
// TraverseReader transforms a ReaderIOResult computation by applying a Reader-based function,
|
||||
// effectively introducing a new environment dependency.
|
||||
//
|
||||
// This function takes a Reader-based transformation (Kleisli arrow) and returns a function that
|
||||
// can transform a ReaderIOResult. The result allows you to provide the Reader's environment (R)
|
||||
// first, which then produces a ReaderIOResult that depends on the context.
|
||||
//
|
||||
// Type transformation:
|
||||
//
|
||||
// From: ReaderIOResult[A]
|
||||
// = func(context.Context) func() Either[error, A]
|
||||
//
|
||||
// With: reader.Kleisli[R, A, B]
|
||||
// = func(A) func(R) B
|
||||
//
|
||||
// To: func(ReaderIOResult[A]) func(R) ReaderIOResult[B]
|
||||
// = func(ReaderIOResult[A]) func(R) func(context.Context) func() Either[error, B]
|
||||
//
|
||||
// This enables:
|
||||
// 1. Transforming values within a ReaderIOResult using environment-dependent logic
|
||||
// 2. Introducing new environment dependencies into existing computations
|
||||
// 3. Building composable pipelines where transformations depend on configuration or dependencies
|
||||
// 4. Point-free style composition with Reader-based transformations
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The environment type that the Reader depends on
|
||||
// - A: The input value type
|
||||
// - B: The output value type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Reader-based Kleisli arrow that transforms A to B using environment R
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderIOResult[A] and returns a Kleisli[R, B],
|
||||
// which is func(R) ReaderIOResult[B]
|
||||
//
|
||||
// The function preserves error handling and IO effects while adding the Reader environment dependency.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Multiplier int
|
||||
// }
|
||||
//
|
||||
// // A Reader-based transformation that depends on Config
|
||||
// multiply := func(x int) func(Config) int {
|
||||
// return func(cfg Config) int {
|
||||
// return x * cfg.Multiplier
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Original computation that produces an int
|
||||
// computation := Right[int](10)
|
||||
//
|
||||
// // Apply TraverseReader to introduce Config dependency
|
||||
// traversed := TraverseReader[Config, int, int](multiply)
|
||||
// result := traversed(computation)
|
||||
//
|
||||
// // Now we can provide the Config to get the final result
|
||||
// cfg := Config{Multiplier: 5}
|
||||
// ctx := context.Background()
|
||||
// finalResult := result(cfg)(ctx)() // Returns Right(50)
|
||||
//
|
||||
// In point-free style, this enables clean composition:
|
||||
//
|
||||
// var pipeline = F.Flow3(
|
||||
// loadValue, // ReaderIOResult[int]
|
||||
// TraverseReader(multiplyByConfig), // func(Config) ReaderIOResult[int]
|
||||
// applyConfig(cfg), // ReaderIOResult[int]
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func TraverseReader[R, A, B any](
|
||||
f reader.Kleisli[R, A, B],
|
||||
) func(ReaderIOResult[A]) Kleisli[R, B] {
|
||||
return RIOR.TraverseReader[context.Context](f)
|
||||
}
|
||||
333
v2/context/readerioresult/flip_example_test.go
Normal file
333
v2/context/readerioresult/flip_example_test.go
Normal file
@@ -0,0 +1,333 @@
|
||||
// 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_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
// Example_sequenceReader_basicUsage demonstrates the basic usage of SequenceReader
|
||||
// to flip the parameter order, enabling point-free style programming.
|
||||
func Example_sequenceReader_basicUsage() {
|
||||
type Config struct {
|
||||
Multiplier int
|
||||
}
|
||||
|
||||
// A computation that produces a Reader based on context
|
||||
getComputation := func(ctx context.Context) func() either.Either[error, func(Config) int] {
|
||||
return func() either.Either[error, func(Config) int] {
|
||||
// This could check context for cancellation, deadlines, etc.
|
||||
return either.Right[error](func(cfg Config) int {
|
||||
return cfg.Multiplier * 10
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sequence it to flip the parameter order
|
||||
// Now Config comes first, then context
|
||||
sequenced := RIOE.SequenceReader(getComputation)
|
||||
|
||||
// Partially apply the Config - this is the key benefit for point-free style
|
||||
cfg := Config{Multiplier: 5}
|
||||
withConfig := sequenced(cfg)
|
||||
|
||||
// Now we have a ReaderIOResult[int] that can be used with any context
|
||||
ctx := context.Background()
|
||||
result := withConfig(ctx)()
|
||||
|
||||
if value, err := either.Unwrap(result); err == nil {
|
||||
fmt.Println(value)
|
||||
}
|
||||
// Output: 50
|
||||
}
|
||||
|
||||
// Example_sequenceReader_dependencyInjection demonstrates how SequenceReader
|
||||
// enables clean dependency injection patterns in point-free style.
|
||||
func Example_sequenceReader_dependencyInjection() {
|
||||
// Define our dependencies
|
||||
type Database struct {
|
||||
ConnectionString string
|
||||
}
|
||||
|
||||
type UserService struct {
|
||||
db Database
|
||||
}
|
||||
|
||||
// A function that creates a computation requiring a Database
|
||||
makeQuery := func(ctx context.Context) func() either.Either[error, func(Database) string] {
|
||||
return func() either.Either[error, func(Database) string] {
|
||||
return either.Right[error](func(db Database) string {
|
||||
return fmt.Sprintf("Querying %s", db.ConnectionString)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sequence to enable dependency injection
|
||||
queryWithDB := RIOE.SequenceReader(makeQuery)
|
||||
|
||||
// Inject the database dependency
|
||||
db := Database{ConnectionString: "localhost:5432"}
|
||||
query := queryWithDB(db)
|
||||
|
||||
// Execute with context
|
||||
ctx := context.Background()
|
||||
result := query(ctx)()
|
||||
|
||||
if value, err := either.Unwrap(result); err == nil {
|
||||
fmt.Println(value)
|
||||
}
|
||||
// Output: Querying localhost:5432
|
||||
}
|
||||
|
||||
// Example_sequenceReader_pointFreeComposition demonstrates how SequenceReader
|
||||
// enables point-free style composition of computations.
|
||||
func Example_sequenceReader_pointFreeComposition() {
|
||||
type Config struct {
|
||||
BaseValue int
|
||||
}
|
||||
|
||||
// Step 1: Create a computation that produces a Reader
|
||||
step1 := func(ctx context.Context) func() either.Either[error, func(Config) int] {
|
||||
return func() either.Either[error, func(Config) int] {
|
||||
return either.Right[error](func(cfg Config) int {
|
||||
return cfg.BaseValue * 2
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Sequence it to enable partial application
|
||||
sequenced := RIOE.SequenceReader(step1)
|
||||
|
||||
// Step 3: Build a pipeline using point-free style
|
||||
// Partially apply the config
|
||||
cfg := Config{BaseValue: 10}
|
||||
|
||||
// Create a reusable computation with the config baked in
|
||||
computation := F.Pipe1(
|
||||
sequenced(cfg),
|
||||
RIOE.Map(func(x int) int { return x + 5 }),
|
||||
)
|
||||
|
||||
// Execute the pipeline
|
||||
ctx := context.Background()
|
||||
result := computation(ctx)()
|
||||
|
||||
if value, err := either.Unwrap(result); err == nil {
|
||||
fmt.Println(value)
|
||||
}
|
||||
// Output: 25
|
||||
}
|
||||
|
||||
// Example_sequenceReader_multipleEnvironments demonstrates using SequenceReader
|
||||
// to work with multiple environment types in a clean, composable way.
|
||||
func Example_sequenceReader_multipleEnvironments() {
|
||||
type DatabaseConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
type APIConfig struct {
|
||||
Endpoint string
|
||||
APIKey string
|
||||
}
|
||||
|
||||
// Function that needs DatabaseConfig
|
||||
getDatabaseURL := func(ctx context.Context) func() either.Either[error, func(DatabaseConfig) string] {
|
||||
return func() either.Either[error, func(DatabaseConfig) string] {
|
||||
return either.Right[error](func(cfg DatabaseConfig) string {
|
||||
return fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Function that needs APIConfig
|
||||
getAPIURL := func(ctx context.Context) func() either.Either[error, func(APIConfig) string] {
|
||||
return func() either.Either[error, func(APIConfig) string] {
|
||||
return either.Right[error](func(cfg APIConfig) string {
|
||||
return cfg.Endpoint
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sequence both to enable partial application
|
||||
withDBConfig := RIOE.SequenceReader(getDatabaseURL)
|
||||
withAPIConfig := RIOE.SequenceReader(getAPIURL)
|
||||
|
||||
// Partially apply different configs
|
||||
dbCfg := DatabaseConfig{Host: "localhost", Port: 5432}
|
||||
apiCfg := APIConfig{Endpoint: "https://api.example.com", APIKey: "secret"}
|
||||
|
||||
dbQuery := withDBConfig(dbCfg)
|
||||
apiQuery := withAPIConfig(apiCfg)
|
||||
|
||||
// Execute both with the same context
|
||||
ctx := context.Background()
|
||||
|
||||
dbResult := dbQuery(ctx)()
|
||||
apiResult := apiQuery(ctx)()
|
||||
|
||||
if dbURL, err := either.Unwrap(dbResult); err == nil {
|
||||
fmt.Println("Database:", dbURL)
|
||||
}
|
||||
if apiURL, err := either.Unwrap(apiResult); err == nil {
|
||||
fmt.Println("API:", apiURL)
|
||||
}
|
||||
// Output:
|
||||
// Database: localhost:5432
|
||||
// API: https://api.example.com
|
||||
}
|
||||
|
||||
// Example_sequenceReaderResult_errorHandling demonstrates how SequenceReaderResult
|
||||
// enables point-free style with proper error handling at multiple levels.
|
||||
func Example_sequenceReaderResult_errorHandling() {
|
||||
type ValidationConfig struct {
|
||||
MinValue int
|
||||
MaxValue int
|
||||
}
|
||||
|
||||
// A computation that can fail at both outer and inner levels
|
||||
makeValidator := func(ctx context.Context) func() either.Either[error, func(context.Context) either.Either[error, int]] {
|
||||
return func() either.Either[error, func(context.Context) either.Either[error, int]] {
|
||||
// Outer level: check context
|
||||
if ctx.Err() != nil {
|
||||
return either.Left[func(context.Context) either.Either[error, int]](ctx.Err())
|
||||
}
|
||||
|
||||
// Return inner computation
|
||||
return either.Right[error](func(innerCtx context.Context) either.Either[error, int] {
|
||||
// Inner level: perform validation
|
||||
value := 42
|
||||
if value < 0 {
|
||||
return either.Left[int](fmt.Errorf("value too small: %d", value))
|
||||
}
|
||||
if value > 100 {
|
||||
return either.Left[int](fmt.Errorf("value too large: %d", value))
|
||||
}
|
||||
return either.Right[error](value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sequence to enable point-free composition
|
||||
sequenced := RIOE.SequenceReaderResult(makeValidator)
|
||||
|
||||
// Build a pipeline with error handling
|
||||
ctx := context.Background()
|
||||
pipeline := F.Pipe2(
|
||||
sequenced(ctx),
|
||||
RIOE.Map(func(x int) int { return x * 2 }),
|
||||
RIOE.Chain(func(x int) RIOE.ReaderIOResult[string] {
|
||||
return RIOE.Of(fmt.Sprintf("Result: %d", x))
|
||||
}),
|
||||
)
|
||||
|
||||
result := pipeline(ctx)()
|
||||
|
||||
if value, err := either.Unwrap(result); err == nil {
|
||||
fmt.Println(value)
|
||||
}
|
||||
// Output: Result: 84
|
||||
}
|
||||
|
||||
// Example_sequenceReader_partialApplication demonstrates the power of partial
|
||||
// application enabled by SequenceReader for building reusable computations.
|
||||
func Example_sequenceReader_partialApplication() {
|
||||
type ServiceConfig struct {
|
||||
ServiceName string
|
||||
Version string
|
||||
}
|
||||
|
||||
// Create a computation factory
|
||||
makeServiceInfo := func(ctx context.Context) func() either.Either[error, func(ServiceConfig) string] {
|
||||
return func() either.Either[error, func(ServiceConfig) string] {
|
||||
return either.Right[error](func(cfg ServiceConfig) string {
|
||||
return fmt.Sprintf("%s v%s", cfg.ServiceName, cfg.Version)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sequence it
|
||||
sequenced := RIOE.SequenceReader(makeServiceInfo)
|
||||
|
||||
// Create multiple service configurations
|
||||
authConfig := ServiceConfig{ServiceName: "AuthService", Version: "1.0.0"}
|
||||
userConfig := ServiceConfig{ServiceName: "UserService", Version: "2.1.0"}
|
||||
|
||||
// Partially apply each config to create specialized computations
|
||||
getAuthInfo := sequenced(authConfig)
|
||||
getUserInfo := sequenced(userConfig)
|
||||
|
||||
// These can now be reused across different contexts
|
||||
ctx := context.Background()
|
||||
|
||||
authResult := getAuthInfo(ctx)()
|
||||
userResult := getUserInfo(ctx)()
|
||||
|
||||
if auth, err := either.Unwrap(authResult); err == nil {
|
||||
fmt.Println(auth)
|
||||
}
|
||||
if user, err := either.Unwrap(userResult); err == nil {
|
||||
fmt.Println(user)
|
||||
}
|
||||
// Output:
|
||||
// AuthService v1.0.0
|
||||
// UserService v2.1.0
|
||||
}
|
||||
|
||||
// Example_sequenceReader_testingBenefits demonstrates how SequenceReader
|
||||
// makes testing easier by allowing you to inject test dependencies.
|
||||
func Example_sequenceReader_testingBenefits() {
|
||||
// Simple logger that collects messages
|
||||
type SimpleLogger struct {
|
||||
Messages []string
|
||||
}
|
||||
|
||||
// A computation that depends on a logger (using the struct directly)
|
||||
makeLoggingOperation := func(ctx context.Context) func() either.Either[error, func(*SimpleLogger) string] {
|
||||
return func() either.Either[error, func(*SimpleLogger) string] {
|
||||
return either.Right[error](func(logger *SimpleLogger) string {
|
||||
logger.Messages = append(logger.Messages, "Operation started")
|
||||
result := "Success"
|
||||
logger.Messages = append(logger.Messages, fmt.Sprintf("Operation completed: %s", result))
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sequence to enable dependency injection
|
||||
sequenced := RIOE.SequenceReader(makeLoggingOperation)
|
||||
|
||||
// Inject a test logger
|
||||
testLogger := &SimpleLogger{Messages: []string{}}
|
||||
operation := sequenced(testLogger)
|
||||
|
||||
// Execute
|
||||
ctx := context.Background()
|
||||
result := operation(ctx)()
|
||||
|
||||
if value, err := either.Unwrap(result); err == nil {
|
||||
fmt.Println("Result:", value)
|
||||
fmt.Println("Logs:", len(testLogger.Messages))
|
||||
}
|
||||
// Output:
|
||||
// Result: Success
|
||||
// Logs: 2
|
||||
}
|
||||
866
v2/context/readerioresult/flip_test.go
Normal file
866
v2/context/readerioresult/flip_test.go
Normal file
@@ -0,0 +1,866 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSequenceReader(t *testing.T) {
|
||||
t.Run("flips parameter order for simple types", func(t *testing.T) {
|
||||
// Original: ReaderIOResult[Reader[string, int]]
|
||||
// = func(context.Context) func() Either[error, func(string) int]
|
||||
original := func(ctx context.Context) func() Either[Reader[string, int]] {
|
||||
return func() Either[Reader[string, int]] {
|
||||
return either.Right[error](func(s string) int {
|
||||
return 10 + len(s)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sequenced: func(string) func(context.Context) IOResult[int]
|
||||
// The Reader environment (string) is now the first parameter
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test original
|
||||
result1 := original(ctx)()
|
||||
assert.True(t, either.IsRight(result1))
|
||||
innerFunc1, _ := either.Unwrap(result1)
|
||||
value1 := innerFunc1("hello")
|
||||
assert.Equal(t, 15, value1)
|
||||
|
||||
// Test sequenced - note the flipped order: string first, then context
|
||||
result2 := sequenced("hello")(ctx)()
|
||||
assert.True(t, either.IsRight(result2))
|
||||
value2, _ := either.Unwrap(result2)
|
||||
assert.Equal(t, 15, value2)
|
||||
})
|
||||
|
||||
t.Run("flips parameter order for struct types", func(t *testing.T) {
|
||||
type Database struct {
|
||||
ConnectionString string
|
||||
}
|
||||
|
||||
// Original: ReaderIOResult[Reader[Database, string]]
|
||||
query := func(ctx context.Context) func() Either[Reader[Database, string]] {
|
||||
return func() Either[Reader[Database, string]] {
|
||||
if ctx.Err() != nil {
|
||||
return either.Left[Reader[Database, string]](ctx.Err())
|
||||
}
|
||||
return either.Right[error](func(db Database) string {
|
||||
return fmt.Sprintf("Query on %s", db.ConnectionString)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
db := Database{ConnectionString: "localhost:5432"}
|
||||
ctx := context.Background()
|
||||
|
||||
expected := "Query on localhost:5432"
|
||||
|
||||
// Sequence it
|
||||
sequenced := SequenceReader(query)
|
||||
|
||||
// Test original with valid inputs
|
||||
result1 := query(ctx)()
|
||||
assert.True(t, either.IsRight(result1))
|
||||
innerFunc1, _ := either.Unwrap(result1)
|
||||
value1 := innerFunc1(db)
|
||||
assert.Equal(t, expected, value1)
|
||||
|
||||
// Test sequenced with valid inputs - Database first, then context
|
||||
result2 := sequenced(db)(ctx)()
|
||||
assert.True(t, either.IsRight(result2))
|
||||
value2, _ := either.Unwrap(result2)
|
||||
assert.Equal(t, expected, value2)
|
||||
})
|
||||
|
||||
t.Run("preserves outer error", func(t *testing.T) {
|
||||
expectedError := errors.New("outer error")
|
||||
|
||||
// Original that fails at outer level
|
||||
original := func(ctx context.Context) func() Either[Reader[string, int]] {
|
||||
return func() Either[Reader[string, int]] {
|
||||
return either.Left[Reader[string, int]](expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test original with error
|
||||
result1 := original(ctx)()
|
||||
assert.True(t, either.IsLeft(result1))
|
||||
_, err1 := either.Unwrap(result1)
|
||||
assert.Equal(t, expectedError, err1)
|
||||
|
||||
// Test sequenced - the outer error is preserved
|
||||
sequenced := SequenceReader(original)
|
||||
result2 := sequenced("test")(ctx)()
|
||||
assert.True(t, either.IsLeft(result2))
|
||||
_, err2 := either.Unwrap(result2)
|
||||
assert.Equal(t, expectedError, err2)
|
||||
})
|
||||
|
||||
t.Run("preserves computation logic", func(t *testing.T) {
|
||||
// Original function
|
||||
original := func(ctx context.Context) func() Either[Reader[string, int]] {
|
||||
return func() Either[Reader[string, int]] {
|
||||
return either.Right[error](func(s string) int {
|
||||
return 3 * len(s)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Sequence
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
// Test that sequence produces correct results
|
||||
result1 := original(ctx)()
|
||||
innerFunc1, _ := either.Unwrap(result1)
|
||||
value1 := innerFunc1("test")
|
||||
|
||||
result2 := sequenced("test")(ctx)()
|
||||
value2, _ := either.Unwrap(result2)
|
||||
|
||||
assert.Equal(t, value1, value2)
|
||||
assert.Equal(t, 12, value2) // 3 * 4
|
||||
})
|
||||
|
||||
t.Run("works with zero values", func(t *testing.T) {
|
||||
original := func(ctx context.Context) func() Either[Reader[string, int]] {
|
||||
return func() Either[Reader[string, int]] {
|
||||
return either.Right[error](func(s string) int {
|
||||
return len(s)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
// Test with zero values
|
||||
result1 := original(ctx)()
|
||||
innerFunc1, _ := either.Unwrap(result1)
|
||||
value1 := innerFunc1("")
|
||||
assert.Equal(t, 0, value1)
|
||||
|
||||
result2 := sequenced("")(ctx)()
|
||||
value2, _ := either.Unwrap(result2)
|
||||
assert.Equal(t, 0, value2)
|
||||
})
|
||||
|
||||
t.Run("respects context cancellation", func(t *testing.T) {
|
||||
original := func(ctx context.Context) func() Either[Reader[string, int]] {
|
||||
return func() Either[Reader[string, int]] {
|
||||
if ctx.Err() != nil {
|
||||
return either.Left[Reader[string, int]](ctx.Err())
|
||||
}
|
||||
return either.Right[error](func(s string) int {
|
||||
return len(s)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
result := sequenced("test")(ctx)()
|
||||
assert.True(t, either.IsLeft(result))
|
||||
_, err := either.Unwrap(result)
|
||||
assert.Equal(t, context.Canceled, err)
|
||||
})
|
||||
|
||||
t.Run("enables point-free style with partial application", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Multiplier int
|
||||
}
|
||||
|
||||
// Original computation
|
||||
original := func(ctx context.Context) func() Either[Reader[Config, int]] {
|
||||
return func() Either[Reader[Config, int]] {
|
||||
return either.Right[error](func(cfg Config) int {
|
||||
return cfg.Multiplier * 10
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sequence to enable partial application
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
// Partially apply the Config
|
||||
cfg := Config{Multiplier: 5}
|
||||
withConfig := sequenced(cfg)
|
||||
|
||||
// Now we have a ReaderIOResult[int] that can be used in different contexts
|
||||
ctx1 := context.Background()
|
||||
result1 := withConfig(ctx1)()
|
||||
assert.True(t, either.IsRight(result1))
|
||||
value1, _ := either.Unwrap(result1)
|
||||
assert.Equal(t, 50, value1)
|
||||
|
||||
// Can reuse with different context
|
||||
ctx2 := context.Background()
|
||||
result2 := withConfig(ctx2)()
|
||||
assert.True(t, either.IsRight(result2))
|
||||
value2, _ := either.Unwrap(result2)
|
||||
assert.Equal(t, 50, value2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSequenceReaderIO(t *testing.T) {
|
||||
t.Run("flips parameter order for simple types", func(t *testing.T) {
|
||||
// Original: ReaderIOResult[ReaderIO[int]]
|
||||
// = func(context.Context) func() Either[error, func(context.Context) func() int]
|
||||
original := func(ctx context.Context) func() Either[ReaderIO[int]] {
|
||||
return func() Either[ReaderIO[int]] {
|
||||
return either.Right[error](func(innerCtx context.Context) func() int {
|
||||
return func() int {
|
||||
return 20
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
sequenced := SequenceReaderIO(original)
|
||||
|
||||
// Test original
|
||||
result1 := original(ctx)()
|
||||
assert.True(t, either.IsRight(result1))
|
||||
innerFunc1, _ := either.Unwrap(result1)
|
||||
value1 := innerFunc1(ctx)()
|
||||
assert.Equal(t, 20, value1)
|
||||
|
||||
// Test sequenced - context first, then context again for inner ReaderIO
|
||||
result2 := sequenced(ctx)(ctx)()
|
||||
assert.True(t, either.IsRight(result2))
|
||||
value2, _ := either.Unwrap(result2)
|
||||
assert.Equal(t, 20, value2)
|
||||
})
|
||||
|
||||
t.Run("preserves outer error", func(t *testing.T) {
|
||||
expectedError := errors.New("outer error")
|
||||
|
||||
// Original that fails at outer level
|
||||
original := func(ctx context.Context) func() Either[ReaderIO[int]] {
|
||||
return func() Either[ReaderIO[int]] {
|
||||
return either.Left[ReaderIO[int]](expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test original with error
|
||||
result1 := original(ctx)()
|
||||
assert.True(t, either.IsLeft(result1))
|
||||
_, err1 := either.Unwrap(result1)
|
||||
assert.Equal(t, expectedError, err1)
|
||||
|
||||
// Test sequenced - the outer error is preserved
|
||||
sequenced := SequenceReaderIO(original)
|
||||
result2 := sequenced(ctx)(ctx)()
|
||||
assert.True(t, either.IsLeft(result2))
|
||||
_, err2 := either.Unwrap(result2)
|
||||
assert.Equal(t, expectedError, err2)
|
||||
})
|
||||
|
||||
t.Run("respects context cancellation in outer context", func(t *testing.T) {
|
||||
original := func(ctx context.Context) func() Either[ReaderIO[int]] {
|
||||
return func() Either[ReaderIO[int]] {
|
||||
if ctx.Err() != nil {
|
||||
return either.Left[ReaderIO[int]](ctx.Err())
|
||||
}
|
||||
return either.Right[error](func(innerCtx context.Context) func() int {
|
||||
return func() int {
|
||||
return 20
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
sequenced := SequenceReaderIO(original)
|
||||
|
||||
result := sequenced(ctx)(ctx)()
|
||||
assert.True(t, either.IsLeft(result))
|
||||
_, err := either.Unwrap(result)
|
||||
assert.Equal(t, context.Canceled, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSequenceReaderResult(t *testing.T) {
|
||||
t.Run("flips parameter order for simple types", func(t *testing.T) {
|
||||
// Original: ReaderIOResult[ReaderResult[int]]
|
||||
// = func(context.Context) func() Either[error, func(context.Context) Either[error, int]]
|
||||
original := func(ctx context.Context) func() Either[ReaderResult[int]] {
|
||||
return func() Either[ReaderResult[int]] {
|
||||
return either.Right[error](func(innerCtx context.Context) Either[int] {
|
||||
return either.Right[error](20)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
sequenced := SequenceReaderResult(original)
|
||||
|
||||
// Test original
|
||||
result1 := original(ctx)()
|
||||
assert.True(t, either.IsRight(result1))
|
||||
innerFunc1, _ := either.Unwrap(result1)
|
||||
innerResult1 := innerFunc1(ctx)
|
||||
assert.True(t, either.IsRight(innerResult1))
|
||||
value1, _ := either.Unwrap(innerResult1)
|
||||
assert.Equal(t, 20, value1)
|
||||
|
||||
// Test sequenced
|
||||
result2 := sequenced(ctx)(ctx)()
|
||||
assert.True(t, either.IsRight(result2))
|
||||
value2, _ := either.Unwrap(result2)
|
||||
assert.Equal(t, 20, value2)
|
||||
})
|
||||
|
||||
t.Run("preserves outer error", func(t *testing.T) {
|
||||
expectedError := errors.New("outer error")
|
||||
|
||||
// Original that fails at outer level
|
||||
original := func(ctx context.Context) func() Either[ReaderResult[int]] {
|
||||
return func() Either[ReaderResult[int]] {
|
||||
return either.Left[ReaderResult[int]](expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test original with error
|
||||
result1 := original(ctx)()
|
||||
assert.True(t, either.IsLeft(result1))
|
||||
_, err1 := either.Unwrap(result1)
|
||||
assert.Equal(t, expectedError, err1)
|
||||
|
||||
// Test sequenced - the outer error is preserved
|
||||
sequenced := SequenceReaderResult(original)
|
||||
result2 := sequenced(ctx)(ctx)()
|
||||
assert.True(t, either.IsLeft(result2))
|
||||
_, err2 := either.Unwrap(result2)
|
||||
assert.Equal(t, expectedError, err2)
|
||||
})
|
||||
|
||||
t.Run("preserves inner error", func(t *testing.T) {
|
||||
expectedError := errors.New("inner error")
|
||||
|
||||
// Original that fails at inner level
|
||||
original := func(ctx context.Context) func() Either[ReaderResult[int]] {
|
||||
return func() Either[ReaderResult[int]] {
|
||||
return either.Right[error](func(innerCtx context.Context) Either[int] {
|
||||
return either.Left[int](expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test original with inner error
|
||||
result1 := original(ctx)()
|
||||
assert.True(t, either.IsRight(result1))
|
||||
innerFunc1, _ := either.Unwrap(result1)
|
||||
innerResult1 := innerFunc1(ctx)
|
||||
assert.True(t, either.IsLeft(innerResult1))
|
||||
_, innerErr1 := either.Unwrap(innerResult1)
|
||||
assert.Equal(t, expectedError, innerErr1)
|
||||
|
||||
// Test sequenced with inner error
|
||||
sequenced := SequenceReaderResult(original)
|
||||
result2 := sequenced(ctx)(ctx)()
|
||||
assert.True(t, either.IsLeft(result2))
|
||||
_, innerErr2 := either.Unwrap(result2)
|
||||
assert.Equal(t, expectedError, innerErr2)
|
||||
})
|
||||
|
||||
t.Run("handles errors at different levels", func(t *testing.T) {
|
||||
// Original that can fail at both levels
|
||||
makeOriginal := func(x int) ReaderIOResult[ReaderResult[int]] {
|
||||
return func(ctx context.Context) func() Either[ReaderResult[int]] {
|
||||
return func() Either[ReaderResult[int]] {
|
||||
if x < -10 {
|
||||
return either.Left[ReaderResult[int]](errors.New("outer: too negative"))
|
||||
}
|
||||
return either.Right[error](func(innerCtx context.Context) Either[int] {
|
||||
if x < 0 {
|
||||
return either.Left[int](errors.New("inner: negative value"))
|
||||
}
|
||||
return either.Right[error](x * 2)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test outer error
|
||||
sequenced1 := SequenceReaderResult(makeOriginal(-20))
|
||||
result1 := sequenced1(ctx)(ctx)()
|
||||
assert.True(t, either.IsLeft(result1))
|
||||
_, err1 := either.Unwrap(result1)
|
||||
assert.Contains(t, err1.Error(), "outer")
|
||||
|
||||
// Test inner error
|
||||
sequenced2 := SequenceReaderResult(makeOriginal(-5))
|
||||
result2 := sequenced2(ctx)(ctx)()
|
||||
assert.True(t, either.IsLeft(result2))
|
||||
_, err2 := either.Unwrap(result2)
|
||||
assert.Contains(t, err2.Error(), "inner")
|
||||
|
||||
// Test success
|
||||
sequenced3 := SequenceReaderResult(makeOriginal(10))
|
||||
result3 := sequenced3(ctx)(ctx)()
|
||||
assert.True(t, either.IsRight(result3))
|
||||
value3, _ := either.Unwrap(result3)
|
||||
assert.Equal(t, 20, value3)
|
||||
})
|
||||
|
||||
t.Run("respects context cancellation", func(t *testing.T) {
|
||||
original := func(ctx context.Context) func() Either[ReaderResult[int]] {
|
||||
return func() Either[ReaderResult[int]] {
|
||||
if ctx.Err() != nil {
|
||||
return either.Left[ReaderResult[int]](ctx.Err())
|
||||
}
|
||||
return either.Right[error](func(innerCtx context.Context) Either[int] {
|
||||
if innerCtx.Err() != nil {
|
||||
return either.Left[int](innerCtx.Err())
|
||||
}
|
||||
return either.Right[error](20)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
sequenced := SequenceReaderResult(original)
|
||||
|
||||
result := sequenced(ctx)(ctx)()
|
||||
assert.True(t, either.IsLeft(result))
|
||||
_, err := either.Unwrap(result)
|
||||
assert.Equal(t, context.Canceled, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSequenceEdgeCases(t *testing.T) {
|
||||
t.Run("works with empty struct", func(t *testing.T) {
|
||||
type Empty struct{}
|
||||
|
||||
original := func(ctx context.Context) func() Either[Reader[Empty, int]] {
|
||||
return func() Either[Reader[Empty, int]] {
|
||||
return either.Right[error](func(e Empty) int {
|
||||
return 20
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
empty := Empty{}
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
result1 := original(ctx)()
|
||||
innerFunc1, _ := either.Unwrap(result1)
|
||||
value1 := innerFunc1(empty)
|
||||
assert.Equal(t, 20, value1)
|
||||
|
||||
result2 := sequenced(empty)(ctx)()
|
||||
value2, _ := either.Unwrap(result2)
|
||||
assert.Equal(t, 20, value2)
|
||||
})
|
||||
|
||||
t.Run("works with pointer types", func(t *testing.T) {
|
||||
type Data struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
original := func(ctx context.Context) func() Either[Reader[*Data, int]] {
|
||||
return func() Either[Reader[*Data, int]] {
|
||||
return either.Right[error](func(d *Data) int {
|
||||
if d == nil {
|
||||
return 42
|
||||
}
|
||||
return 42 + d.Value
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
data := &Data{Value: 100}
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
// Test with non-nil pointer
|
||||
result1 := original(ctx)()
|
||||
innerFunc1, _ := either.Unwrap(result1)
|
||||
value1 := innerFunc1(data)
|
||||
assert.Equal(t, 142, value1)
|
||||
|
||||
result2 := sequenced(data)(ctx)()
|
||||
value2, _ := either.Unwrap(result2)
|
||||
assert.Equal(t, 142, value2)
|
||||
|
||||
// Test with nil pointer
|
||||
result3 := sequenced(nil)(ctx)()
|
||||
value3, _ := either.Unwrap(result3)
|
||||
assert.Equal(t, 42, value3)
|
||||
})
|
||||
|
||||
t.Run("maintains referential transparency", func(t *testing.T) {
|
||||
// The same inputs should always produce the same outputs
|
||||
original := func(ctx context.Context) func() Either[Reader[string, int]] {
|
||||
return func() Either[Reader[string, int]] {
|
||||
return either.Right[error](func(s string) int {
|
||||
return 10 + len(s)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
// Call multiple times with same inputs
|
||||
for range 5 {
|
||||
result1 := original(ctx)()
|
||||
innerFunc1, _ := either.Unwrap(result1)
|
||||
value1 := innerFunc1("hello")
|
||||
assert.Equal(t, 15, value1)
|
||||
|
||||
result2 := sequenced("hello")(ctx)()
|
||||
value2, _ := either.Unwrap(result2)
|
||||
assert.Equal(t, 15, value2)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestTraverseReader(t *testing.T) {
|
||||
t.Run("basic transformation with Reader dependency", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Multiplier int
|
||||
}
|
||||
|
||||
// Original computation
|
||||
original := Right(10)
|
||||
|
||||
// Reader-based transformation
|
||||
multiply := func(x int) Reader[Config, int] {
|
||||
return func(cfg Config) int {
|
||||
return x * cfg.Multiplier
|
||||
}
|
||||
}
|
||||
|
||||
// Apply TraverseReader
|
||||
traversed := TraverseReader(multiply)
|
||||
result := traversed(original)
|
||||
|
||||
// Provide Config and execute
|
||||
cfg := Config{Multiplier: 5}
|
||||
ctx := context.Background()
|
||||
finalResult := result(cfg)(ctx)()
|
||||
|
||||
assert.True(t, either.IsRight(finalResult))
|
||||
value, _ := either.Unwrap(finalResult)
|
||||
assert.Equal(t, 50, value)
|
||||
})
|
||||
|
||||
t.Run("preserves outer error", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Multiplier int
|
||||
}
|
||||
|
||||
expectedError := errors.New("computation failed")
|
||||
|
||||
// Original computation that fails
|
||||
original := Left[int](expectedError)
|
||||
|
||||
// Reader-based transformation (won't be called)
|
||||
multiply := func(x int) Reader[Config, int] {
|
||||
return func(cfg Config) int {
|
||||
return x * cfg.Multiplier
|
||||
}
|
||||
}
|
||||
|
||||
// Apply TraverseReader
|
||||
traversed := TraverseReader(multiply)
|
||||
result := traversed(original)
|
||||
|
||||
// Provide Config and execute
|
||||
cfg := Config{Multiplier: 5}
|
||||
ctx := context.Background()
|
||||
finalResult := result(cfg)(ctx)()
|
||||
|
||||
assert.True(t, either.IsLeft(finalResult))
|
||||
_, err := either.Unwrap(finalResult)
|
||||
assert.Equal(t, expectedError, err)
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
type Database struct {
|
||||
Prefix string
|
||||
}
|
||||
|
||||
// Original computation producing an int
|
||||
original := Right(42)
|
||||
|
||||
// Reader-based transformation: int -> string using Database
|
||||
format := func(x int) func(Database) string {
|
||||
return func(db Database) string {
|
||||
return fmt.Sprintf("%s:%d", db.Prefix, x)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply TraverseReader
|
||||
traversed := TraverseReader(format)
|
||||
result := traversed(original)
|
||||
|
||||
// Provide Database and execute
|
||||
db := Database{Prefix: "ID"}
|
||||
ctx := context.Background()
|
||||
finalResult := result(db)(ctx)()
|
||||
|
||||
assert.True(t, either.IsRight(finalResult))
|
||||
value, _ := either.Unwrap(finalResult)
|
||||
assert.Equal(t, "ID:42", value)
|
||||
})
|
||||
|
||||
t.Run("works with struct environments", func(t *testing.T) {
|
||||
type Settings struct {
|
||||
Prefix string
|
||||
Suffix string
|
||||
}
|
||||
|
||||
// Original computation
|
||||
original := Right("value")
|
||||
|
||||
// Reader-based transformation using Settings
|
||||
decorate := func(s string) func(Settings) string {
|
||||
return func(settings Settings) string {
|
||||
return settings.Prefix + s + settings.Suffix
|
||||
}
|
||||
}
|
||||
|
||||
// Apply TraverseReader
|
||||
traversed := TraverseReader(decorate)
|
||||
result := traversed(original)
|
||||
|
||||
// Provide Settings and execute
|
||||
settings := Settings{Prefix: "[", Suffix: "]"}
|
||||
ctx := context.Background()
|
||||
finalResult := result(settings)(ctx)()
|
||||
|
||||
assert.True(t, either.IsRight(finalResult))
|
||||
value, _ := either.Unwrap(finalResult)
|
||||
assert.Equal(t, "[value]", value)
|
||||
})
|
||||
|
||||
t.Run("enables partial application", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Factor int
|
||||
}
|
||||
|
||||
// Original computation
|
||||
original := Right(10)
|
||||
|
||||
// Reader-based transformation
|
||||
scale := func(x int) Reader[Config, int] {
|
||||
return func(cfg Config) int {
|
||||
return x * cfg.Factor
|
||||
}
|
||||
}
|
||||
|
||||
// Apply TraverseReader
|
||||
traversed := TraverseReader(scale)
|
||||
result := traversed(original)
|
||||
|
||||
// Partially apply Config
|
||||
cfg := Config{Factor: 3}
|
||||
withConfig := result(cfg)
|
||||
|
||||
// Can now use with different contexts
|
||||
ctx1 := context.Background()
|
||||
finalResult1 := withConfig(ctx1)()
|
||||
assert.True(t, either.IsRight(finalResult1))
|
||||
value1, _ := either.Unwrap(finalResult1)
|
||||
assert.Equal(t, 30, value1)
|
||||
|
||||
// Reuse with different context
|
||||
ctx2 := context.Background()
|
||||
finalResult2 := withConfig(ctx2)()
|
||||
assert.True(t, either.IsRight(finalResult2))
|
||||
value2, _ := either.Unwrap(finalResult2)
|
||||
assert.Equal(t, 30, value2)
|
||||
})
|
||||
|
||||
t.Run("respects context cancellation", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
// Original computation that checks context
|
||||
original := func(ctx context.Context) func() Either[int] {
|
||||
return func() Either[int] {
|
||||
if ctx.Err() != nil {
|
||||
return either.Left[int](ctx.Err())
|
||||
}
|
||||
return either.Right[error](10)
|
||||
}
|
||||
}
|
||||
|
||||
// Reader-based transformation
|
||||
multiply := func(x int) Reader[Config, int] {
|
||||
return func(cfg Config) int {
|
||||
return x * cfg.Value
|
||||
}
|
||||
}
|
||||
|
||||
// Apply TraverseReader
|
||||
traversed := TraverseReader(multiply)
|
||||
result := traversed(original)
|
||||
|
||||
// Use canceled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
cfg := Config{Value: 5}
|
||||
finalResult := result(cfg)(ctx)()
|
||||
|
||||
assert.True(t, either.IsLeft(finalResult))
|
||||
_, err := either.Unwrap(finalResult)
|
||||
assert.Equal(t, context.Canceled, err)
|
||||
})
|
||||
|
||||
t.Run("works with zero values", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Offset int
|
||||
}
|
||||
|
||||
// Original computation with zero value
|
||||
original := Right(0)
|
||||
|
||||
// Reader-based transformation
|
||||
add := func(x int) Reader[Config, int] {
|
||||
return func(cfg Config) int {
|
||||
return x + cfg.Offset
|
||||
}
|
||||
}
|
||||
|
||||
// Apply TraverseReader
|
||||
traversed := TraverseReader(add)
|
||||
result := traversed(original)
|
||||
|
||||
// Provide Config with zero offset
|
||||
cfg := Config{Offset: 0}
|
||||
ctx := context.Background()
|
||||
finalResult := result(cfg)(ctx)()
|
||||
|
||||
assert.True(t, either.IsRight(finalResult))
|
||||
value, _ := either.Unwrap(finalResult)
|
||||
assert.Equal(t, 0, value)
|
||||
})
|
||||
|
||||
t.Run("chains multiple transformations", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Multiplier int
|
||||
}
|
||||
|
||||
// Original computation
|
||||
original := Right(5)
|
||||
|
||||
// First Reader-based transformation
|
||||
multiply := func(x int) Reader[Config, int] {
|
||||
return func(cfg Config) int {
|
||||
return x * cfg.Multiplier
|
||||
}
|
||||
}
|
||||
|
||||
// Apply TraverseReader
|
||||
traversed := TraverseReader(multiply)
|
||||
result := traversed(original)
|
||||
|
||||
// Provide Config and execute
|
||||
cfg := Config{Multiplier: 4}
|
||||
ctx := context.Background()
|
||||
finalResult := result(cfg)(ctx)()
|
||||
|
||||
assert.True(t, either.IsRight(finalResult))
|
||||
value, _ := either.Unwrap(finalResult)
|
||||
assert.Equal(t, 20, value) // 5 * 4 = 20
|
||||
})
|
||||
|
||||
t.Run("works with complex Reader logic", func(t *testing.T) {
|
||||
type ValidationRules struct {
|
||||
MinValue int
|
||||
MaxValue int
|
||||
}
|
||||
|
||||
// Original computation
|
||||
original := Right(50)
|
||||
|
||||
// Reader-based transformation with validation logic
|
||||
validate := func(x int) func(ValidationRules) int {
|
||||
return func(rules ValidationRules) int {
|
||||
if x < rules.MinValue {
|
||||
return rules.MinValue
|
||||
}
|
||||
if x > rules.MaxValue {
|
||||
return rules.MaxValue
|
||||
}
|
||||
return x
|
||||
}
|
||||
}
|
||||
|
||||
// Apply TraverseReader
|
||||
traversed := TraverseReader(validate)
|
||||
result := traversed(original)
|
||||
|
||||
// Test with value within range
|
||||
rules1 := ValidationRules{MinValue: 0, MaxValue: 100}
|
||||
ctx := context.Background()
|
||||
finalResult1 := result(rules1)(ctx)()
|
||||
assert.True(t, either.IsRight(finalResult1))
|
||||
value1, _ := either.Unwrap(finalResult1)
|
||||
assert.Equal(t, 50, value1)
|
||||
|
||||
// Test with value above max
|
||||
rules2 := ValidationRules{MinValue: 0, MaxValue: 30}
|
||||
finalResult2 := result(rules2)(ctx)()
|
||||
assert.True(t, either.IsRight(finalResult2))
|
||||
value2, _ := either.Unwrap(finalResult2)
|
||||
assert.Equal(t, 30, value2) // Clamped to max
|
||||
|
||||
// Test with value below min
|
||||
rules3 := ValidationRules{MinValue: 60, MaxValue: 100}
|
||||
finalResult3 := result(rules3)(ctx)()
|
||||
assert.True(t, either.IsRight(finalResult3))
|
||||
value3, _ := either.Unwrap(finalResult3)
|
||||
assert.Equal(t, 60, value3) // Clamped to min
|
||||
})
|
||||
}
|
||||
1869
v2/context/readerioresult/gen.go
Normal file
1869
v2/context/readerioresult/gen.go
Normal file
File diff suppressed because it is too large
Load Diff
155
v2/context/readerioresult/http/builder/builder.go
Normal file
155
v2/context/readerioresult/http/builder/builder.go
Normal file
@@ -0,0 +1,155 @@
|
||||
// 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 provides utilities for building HTTP requests in a functional way
|
||||
// using the ReaderIOResult monad. It integrates with the http/builder package to
|
||||
// create composable, type-safe HTTP request builders with proper error handling
|
||||
// and context support.
|
||||
//
|
||||
// The main function, Requester, converts a Builder from the http/builder package
|
||||
// into a ReaderIOResult that produces HTTP requests. This allows for:
|
||||
// - Immutable request building with method chaining
|
||||
// - Automatic header management including Content-Length
|
||||
// - Support for requests with and without bodies
|
||||
// - Proper error handling wrapped in Either
|
||||
// - Context propagation for cancellation and timeouts
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// import (
|
||||
// "context"
|
||||
// B "github.com/IBM/fp-go/v2/http/builder"
|
||||
// RB "github.com/IBM/fp-go/v2/context/readerioresult/http/builder"
|
||||
// )
|
||||
//
|
||||
// builder := F.Pipe3(
|
||||
// B.Default,
|
||||
// B.WithURL("https://api.example.com/users"),
|
||||
// B.WithMethod("POST"),
|
||||
// B.WithJSONBody(userData),
|
||||
// )
|
||||
//
|
||||
// requester := RB.Requester(builder)
|
||||
// result := requester(context.Background())()
|
||||
package builder
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
RIOEH "github.com/IBM/fp-go/v2/context/readerioresult/http"
|
||||
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"
|
||||
LZ "github.com/IBM/fp-go/v2/lazy"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// Requester converts an http/builder.Builder into a ReaderIOResult that produces HTTP requests.
|
||||
// It handles both requests with and without bodies, automatically managing headers including
|
||||
// Content-Length for requests with bodies.
|
||||
//
|
||||
// The function performs the following operations:
|
||||
// 1. Extracts the request body (if present) from the builder
|
||||
// 2. Creates appropriate request constructor (with or without body)
|
||||
// 3. Applies the target URL from the builder
|
||||
// 4. Applies the HTTP method from the builder
|
||||
// 5. Merges headers from the builder into the request
|
||||
// 6. Handles any errors that occur during request construction
|
||||
//
|
||||
// For requests with a body:
|
||||
// - Sets the Content-Length header automatically
|
||||
// - Uses bytes.NewReader to create the request body
|
||||
// - Merges builder headers into the request
|
||||
//
|
||||
// For requests without a body:
|
||||
// - Creates a request with nil body
|
||||
// - Merges builder headers into the request
|
||||
//
|
||||
// Parameters:
|
||||
// - builder: A pointer to an http/builder.Builder containing request configuration
|
||||
//
|
||||
// Returns:
|
||||
// - A Requester (ReaderIOResult[*http.Request]) that, when executed with a context,
|
||||
// produces either an error or a configured *http.Request
|
||||
//
|
||||
// Example with body:
|
||||
//
|
||||
// import (
|
||||
// B "github.com/IBM/fp-go/v2/http/builder"
|
||||
// RB "github.com/IBM/fp-go/v2/context/readerioresult/http/builder"
|
||||
// )
|
||||
//
|
||||
// builder := F.Pipe3(
|
||||
// B.Default,
|
||||
// B.WithURL("https://api.example.com/users"),
|
||||
// B.WithMethod("POST"),
|
||||
// B.WithJSONBody(map[string]string{"name": "John"}),
|
||||
// )
|
||||
// requester := RB.Requester(builder)
|
||||
// result := requester(context.Background())()
|
||||
//
|
||||
// Example without body:
|
||||
//
|
||||
// builder := F.Pipe2(
|
||||
// B.Default,
|
||||
// B.WithURL("https://api.example.com/users"),
|
||||
// B.WithMethod("GET"),
|
||||
// )
|
||||
// requester := RB.Requester(builder)
|
||||
// result := requester(context.Background())()
|
||||
func Requester(builder *R.Builder) RIOEH.Requester {
|
||||
|
||||
withBody := F.Curry3(func(data []byte, url string, method string) RIOE.ReaderIOResult[*http.Request] {
|
||||
return RIOE.TryCatch(func(ctx context.Context) func() (*http.Request, error) {
|
||||
return func() (*http.Request, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, 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) RIOE.ReaderIOResult[*http.Request] {
|
||||
return RIOE.TryCatch(func(ctx context.Context) func() (*http.Request, error) {
|
||||
return func() (*http.Request, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, nil)
|
||||
if err == nil {
|
||||
H.Monoid.Concat(req.Header, builder.GetHeaders())
|
||||
}
|
||||
return req, err
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return F.Pipe5(
|
||||
builder.GetBody(),
|
||||
O.Fold(LZ.Of(result.Of(withoutBody)), result.Map(withBody)),
|
||||
result.Ap[RIOE.Kleisli[string, *http.Request]](builder.GetTargetURL()),
|
||||
result.Flap[RIOE.ReaderIOResult[*http.Request]](builder.GetMethod()),
|
||||
result.GetOrElse(RIOE.Left[*http.Request]),
|
||||
RIOE.Map(func(req *http.Request) *http.Request {
|
||||
req.Header = H.Monoid.Concat(req.Header, builder.GetHeaders())
|
||||
return req
|
||||
}),
|
||||
)
|
||||
}
|
||||
287
v2/context/readerioresult/http/builder/builder_test.go
Normal file
287
v2/context/readerioresult/http/builder/builder_test.go
Normal file
@@ -0,0 +1,287 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
R "github.com/IBM/fp-go/v2/http/builder"
|
||||
IO "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,
|
||||
RIOE.Map(func(r *http.Request) *url.URL {
|
||||
return r.URL
|
||||
}),
|
||||
RIOE.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(context.Background())()))
|
||||
}
|
||||
|
||||
// TestBuilderWithoutBody tests creating a request without a body
|
||||
func TestBuilderWithoutBody(t *testing.T) {
|
||||
builder := F.Pipe2(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/users"),
|
||||
R.WithMethod("GET"),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
|
||||
assert.NotNil(t, req, "Expected non-nil request")
|
||||
assert.Equal(t, "GET", req.Method)
|
||||
assert.Equal(t, "https://api.example.com/users", req.URL.String())
|
||||
assert.Nil(t, req.Body, "Expected nil body for GET request")
|
||||
}
|
||||
|
||||
// TestBuilderWithBody tests creating a request with a body
|
||||
func TestBuilderWithBody(t *testing.T) {
|
||||
bodyData := []byte(`{"name":"John","age":30}`)
|
||||
|
||||
builder := F.Pipe3(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/users"),
|
||||
R.WithMethod("POST"),
|
||||
R.WithBytes(bodyData),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
|
||||
assert.NotNil(t, req, "Expected non-nil request")
|
||||
assert.Equal(t, "POST", req.Method)
|
||||
assert.Equal(t, "https://api.example.com/users", req.URL.String())
|
||||
assert.NotNil(t, req.Body, "Expected non-nil body for POST request")
|
||||
assert.Equal(t, "24", req.Header.Get("Content-Length"))
|
||||
}
|
||||
|
||||
// TestBuilderWithHeaders tests that headers are properly set
|
||||
func TestBuilderWithHeaders(t *testing.T) {
|
||||
builder := F.Pipe3(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/data"),
|
||||
R.WithHeader("Authorization")("Bearer token123"),
|
||||
R.WithHeader("Accept")("application/json"),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
|
||||
assert.NotNil(t, req, "Expected non-nil request")
|
||||
assert.Equal(t, "Bearer token123", req.Header.Get("Authorization"))
|
||||
assert.Equal(t, "application/json", req.Header.Get("Accept"))
|
||||
}
|
||||
|
||||
// TestBuilderWithInvalidURL tests error handling for invalid URLs
|
||||
func TestBuilderWithInvalidURL(t *testing.T) {
|
||||
builder := F.Pipe1(
|
||||
R.Default,
|
||||
R.WithURL("://invalid-url"),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
assert.True(t, E.IsLeft(result), "Expected Left result for invalid URL")
|
||||
}
|
||||
|
||||
// TestBuilderWithEmptyMethod tests creating a request with empty method
|
||||
func TestBuilderWithEmptyMethod(t *testing.T) {
|
||||
builder := F.Pipe2(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/users"),
|
||||
R.WithMethod(""),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
// Empty method should still work (defaults to GET in http.NewRequest)
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
}
|
||||
|
||||
// TestBuilderWithMultipleHeaders tests setting multiple headers
|
||||
func TestBuilderWithMultipleHeaders(t *testing.T) {
|
||||
builder := F.Pipe4(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/data"),
|
||||
R.WithHeader("X-Custom-Header-1")("value1"),
|
||||
R.WithHeader("X-Custom-Header-2")("value2"),
|
||||
R.WithHeader("X-Custom-Header-3")("value3"),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
|
||||
assert.NotNil(t, req, "Expected non-nil request")
|
||||
assert.Equal(t, "value1", req.Header.Get("X-Custom-Header-1"))
|
||||
assert.Equal(t, "value2", req.Header.Get("X-Custom-Header-2"))
|
||||
assert.Equal(t, "value3", req.Header.Get("X-Custom-Header-3"))
|
||||
}
|
||||
|
||||
// TestBuilderWithBodyAndHeaders tests combining body and headers
|
||||
func TestBuilderWithBodyAndHeaders(t *testing.T) {
|
||||
bodyData := []byte(`{"test":"data"}`)
|
||||
|
||||
builder := F.Pipe4(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/submit"),
|
||||
R.WithMethod("PUT"),
|
||||
R.WithBytes(bodyData),
|
||||
R.WithHeader("X-Request-ID")("12345"),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
|
||||
assert.NotNil(t, req, "Expected non-nil request")
|
||||
assert.Equal(t, "PUT", req.Method)
|
||||
assert.NotNil(t, req.Body, "Expected non-nil body")
|
||||
assert.Equal(t, "12345", req.Header.Get("X-Request-ID"))
|
||||
assert.Equal(t, "15", req.Header.Get("Content-Length"))
|
||||
}
|
||||
|
||||
// TestBuilderContextCancellation tests that context cancellation is respected
|
||||
func TestBuilderContextCancellation(t *testing.T) {
|
||||
builder := F.Pipe1(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/users"),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
|
||||
// Create a cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // Cancel immediately
|
||||
|
||||
result := requester(ctx)()
|
||||
|
||||
// The request should still be created (cancellation affects execution, not creation)
|
||||
// But we verify the context is properly passed
|
||||
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
|
||||
if req != nil {
|
||||
assert.Equal(t, ctx, req.Context(), "Expected context to be set in request")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuilderWithDifferentMethods tests various HTTP methods
|
||||
func TestBuilderWithDifferentMethods(t *testing.T) {
|
||||
methods := []string{"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"}
|
||||
|
||||
for _, method := range methods {
|
||||
t.Run(method, func(t *testing.T) {
|
||||
builder := F.Pipe2(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/resource"),
|
||||
R.WithMethod(method),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result for method %s", method)
|
||||
|
||||
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
|
||||
assert.NotNil(t, req, "Expected non-nil request for method %s", method)
|
||||
assert.Equal(t, method, req.Method)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuilderWithJSON tests creating a request with JSON body
|
||||
func TestBuilderWithJSON(t *testing.T) {
|
||||
data := map[string]string{"username": "testuser", "email": "test@example.com"}
|
||||
|
||||
builder := F.Pipe3(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/v1/users"),
|
||||
R.WithMethod("POST"),
|
||||
R.WithJSON(data),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
|
||||
assert.NotNil(t, req, "Expected non-nil request")
|
||||
assert.Equal(t, "POST", req.Method)
|
||||
assert.Equal(t, "https://api.example.com/v1/users", req.URL.String())
|
||||
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
|
||||
assert.NotNil(t, req.Body)
|
||||
}
|
||||
|
||||
// TestBuilderWithBearer tests adding Bearer token
|
||||
func TestBuilderWithBearer(t *testing.T) {
|
||||
builder := F.Pipe2(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/protected"),
|
||||
R.WithBearer("my-secret-token"),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
|
||||
assert.NotNil(t, req, "Expected non-nil request")
|
||||
assert.Equal(t, "Bearer my-secret-token", req.Header.Get("Authorization"))
|
||||
}
|
||||
15
v2/context/readerioresult/http/builder/coverage.out
Normal file
15
v2/context/readerioresult/http/builder/coverage.out
Normal file
@@ -0,0 +1,15 @@
|
||||
mode: set
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/http/builder/builder.go:117.52,119.103 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/http/builder/builder.go:119.103,120.80 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/http/builder/builder.go:120.80,121.41 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/http/builder/builder.go:121.41,123.19 2 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/http/builder/builder.go:123.19,126.6 2 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/http/builder/builder.go:127.5,127.20 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/http/builder/builder.go:132.2,132.93 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/http/builder/builder.go:132.93,133.80 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/http/builder/builder.go:133.80,134.41 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/http/builder/builder.go:134.41,136.19 2 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/http/builder/builder.go:136.19,138.6 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/http/builder/builder.go:139.5,139.20 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/http/builder/builder.go:144.2,150.50 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/http/builder/builder.go:150.50,153.4 2 1
|
||||
11
v2/context/readerioresult/http/coverage.out
Normal file
11
v2/context/readerioresult/http/coverage.out
Normal file
@@ -0,0 +1,11 @@
|
||||
mode: set
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/http/request.go:111.76,116.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/http/request.go:134.49,136.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/http/request.go:161.90,162.65 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/http/request.go:162.65,166.76 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/http/request.go:166.76,176.5 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/http/request.go:198.73,203.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/http/request.go:222.74,227.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/http/request.go:234.76,236.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/http/request.go:245.74,254.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/http/request.go:281.76,286.2 1 1
|
||||
286
v2/context/readerioresult/http/request.go
Normal file
286
v2/context/readerioresult/http/request.go
Normal file
@@ -0,0 +1,286 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package http provides functional HTTP client utilities built on top of ReaderIOResult monad.
|
||||
// It offers a composable way to make HTTP requests with context support, error handling,
|
||||
// and response parsing capabilities. The package follows functional programming principles
|
||||
// to ensure type-safe, testable, and maintainable HTTP operations.
|
||||
//
|
||||
// The main abstractions include:
|
||||
// - Requester: A reader that constructs HTTP requests with context
|
||||
// - Client: An interface for executing HTTP requests
|
||||
// - Response readers: Functions to parse responses as bytes, text, or JSON
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/data")
|
||||
// result := ReadJSON[MyType](client)(request)
|
||||
// response := result(context.Background())()
|
||||
package http
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
B "github.com/IBM/fp-go/v2/bytes"
|
||||
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
H "github.com/IBM/fp-go/v2/http"
|
||||
IOE "github.com/IBM/fp-go/v2/ioeither"
|
||||
IOEF "github.com/IBM/fp-go/v2/ioeither/file"
|
||||
J "github.com/IBM/fp-go/v2/json"
|
||||
P "github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
type (
|
||||
// Requester is a reader that constructs an HTTP request with context support.
|
||||
// It represents a computation that, given a context, produces either an error
|
||||
// or an HTTP request. This allows for composable request building with proper
|
||||
// error handling and context propagation.
|
||||
Requester = RIOE.ReaderIOResult[*http.Request]
|
||||
|
||||
// Client is an interface for executing HTTP requests in a functional way.
|
||||
// It wraps the standard http.Client and provides a Do method that works
|
||||
// with the ReaderIOResult monad for composable, type-safe HTTP operations.
|
||||
Client interface {
|
||||
// Do executes an HTTP request and returns the response wrapped in a ReaderIOResult.
|
||||
// It takes a Requester (which builds the request) and returns a computation that,
|
||||
// when executed with a context, performs the HTTP request and returns either
|
||||
// an error or the HTTP response.
|
||||
//
|
||||
// Parameters:
|
||||
// - req: A Requester that builds the HTTP request
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderIOResult that produces either an error or an *http.Response
|
||||
Do(Requester) RIOE.ReaderIOResult[*http.Response]
|
||||
}
|
||||
|
||||
// client is the internal implementation of the Client interface.
|
||||
// It wraps a standard http.Client and provides functional HTTP operations.
|
||||
client struct {
|
||||
delegate *http.Client
|
||||
doIOE IOE.Kleisli[error, *http.Request, *http.Response]
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
// MakeRequest is an eitherized version of http.NewRequestWithContext.
|
||||
// It creates a Requester that builds an HTTP request with the given method, URL, and body.
|
||||
// This function properly handles errors and wraps them in the Either monad.
|
||||
//
|
||||
// Parameters:
|
||||
// - method: HTTP method (GET, POST, PUT, DELETE, etc.)
|
||||
// - url: The target URL for the request
|
||||
// - body: Optional request body (can be nil)
|
||||
//
|
||||
// Returns:
|
||||
// - A Requester that produces either an error or an *http.Request
|
||||
MakeRequest = RIOE.Eitherize3(http.NewRequestWithContext)
|
||||
|
||||
// makeRequest is a partially applied version of MakeRequest with the context parameter bound.
|
||||
makeRequest = F.Bind13of3(MakeRequest)
|
||||
|
||||
// MakeGetRequest creates a GET request for the specified URL.
|
||||
// It's a convenience function that specializes MakeRequest for GET requests with no body.
|
||||
//
|
||||
// Parameters:
|
||||
// - url: The target URL for the GET request
|
||||
//
|
||||
// Returns:
|
||||
// - A Requester that produces either an error or an *http.Request
|
||||
//
|
||||
// Example:
|
||||
// req := MakeGetRequest("https://api.example.com/users")
|
||||
MakeGetRequest = makeRequest("GET", nil)
|
||||
)
|
||||
|
||||
func (client client) Do(req Requester) RIOE.ReaderIOResult[*http.Response] {
|
||||
return F.Pipe1(
|
||||
req,
|
||||
RIOE.ChainIOEitherK(client.doIOE),
|
||||
)
|
||||
}
|
||||
|
||||
// MakeClient creates a functional HTTP client wrapper around a standard http.Client.
|
||||
// The returned Client provides methods for executing HTTP requests in a functional,
|
||||
// composable way using the ReaderIOResult monad.
|
||||
//
|
||||
// Parameters:
|
||||
// - httpClient: A standard *http.Client to wrap (e.g., http.DefaultClient)
|
||||
//
|
||||
// Returns:
|
||||
// - A Client that can execute HTTP requests functionally
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// // or with custom client
|
||||
// customClient := &http.Client{Timeout: 10 * time.Second}
|
||||
// client := MakeClient(customClient)
|
||||
func MakeClient(httpClient *http.Client) Client {
|
||||
return client{delegate: httpClient, doIOE: IOE.Eitherize1(httpClient.Do)}
|
||||
}
|
||||
|
||||
// ReadFullResponse sends an HTTP request, reads the complete response body as a byte array,
|
||||
// and returns both the response and body as a tuple (FullResponse).
|
||||
// It validates the HTTP status code and handles errors appropriately.
|
||||
//
|
||||
// The function performs the following steps:
|
||||
// 1. Executes the HTTP request using the provided client
|
||||
// 2. Validates the response status code (checks for HTTP errors)
|
||||
// 3. Reads the entire response body into a byte array
|
||||
// 4. Returns a tuple containing the response and body
|
||||
//
|
||||
// Parameters:
|
||||
// - client: The HTTP client to use for executing the request
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Requester and returns a ReaderIOResult[FullResponse]
|
||||
// where FullResponse is a tuple of (*http.Response, []byte)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/data")
|
||||
// fullResp := ReadFullResponse(client)(request)
|
||||
// result := fullResp(context.Background())()
|
||||
func ReadFullResponse(client Client) RIOE.Kleisli[Requester, H.FullResponse] {
|
||||
return func(req Requester) RIOE.ReaderIOResult[H.FullResponse] {
|
||||
return F.Flow3(
|
||||
client.Do(req),
|
||||
IOE.ChainEitherK(H.ValidateResponse),
|
||||
IOE.Chain(func(resp *http.Response) IOE.IOEither[error, H.FullResponse] {
|
||||
return F.Pipe1(
|
||||
F.Pipe3(
|
||||
resp,
|
||||
H.GetBody,
|
||||
IOE.Of[error, io.ReadCloser],
|
||||
IOEF.ReadAll[io.ReadCloser],
|
||||
),
|
||||
IOE.Map[error](F.Bind1st(P.MakePair[*http.Response, []byte], resp)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ReadAll sends an HTTP request and reads the complete response body as a byte array.
|
||||
// It validates the HTTP status code and returns the raw response body bytes.
|
||||
// This is useful when you need to process the response body in a custom way.
|
||||
//
|
||||
// Parameters:
|
||||
// - client: The HTTP client to use for executing the request
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Requester and returns a ReaderIOResult[[]byte]
|
||||
// containing the response body as bytes
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/data")
|
||||
// readBytes := ReadAll(client)
|
||||
// result := readBytes(request)(context.Background())()
|
||||
func ReadAll(client Client) RIOE.Kleisli[Requester, []byte] {
|
||||
return F.Flow2(
|
||||
ReadFullResponse(client),
|
||||
RIOE.Map(H.Body),
|
||||
)
|
||||
}
|
||||
|
||||
// ReadText sends an HTTP request, reads the response body, and converts it to a string.
|
||||
// It validates the HTTP status code and returns the response body as a UTF-8 string.
|
||||
// This is convenient for APIs that return plain text responses.
|
||||
//
|
||||
// Parameters:
|
||||
// - client: The HTTP client to use for executing the request
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Requester and returns a ReaderIOResult[string]
|
||||
// containing the response body as a string
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/text")
|
||||
// readText := ReadText(client)
|
||||
// result := readText(request)(context.Background())()
|
||||
func ReadText(client Client) RIOE.Kleisli[Requester, string] {
|
||||
return F.Flow2(
|
||||
ReadAll(client),
|
||||
RIOE.Map(B.ToString),
|
||||
)
|
||||
}
|
||||
|
||||
// ReadJson sends an HTTP request, reads the response, and parses it as JSON.
|
||||
//
|
||||
// Deprecated: Use [ReadJSON] instead. This function is kept for backward compatibility
|
||||
// but will be removed in a future version. The capitalized version follows Go naming
|
||||
// conventions for acronyms.
|
||||
func ReadJson[A any](client Client) RIOE.Kleisli[Requester, A] {
|
||||
return ReadJSON[A](client)
|
||||
}
|
||||
|
||||
// readJSON is an internal helper that reads the response body and validates JSON content type.
|
||||
// It performs the following validations:
|
||||
// 1. Validates HTTP status code
|
||||
// 2. Validates that the response Content-Type is application/json
|
||||
// 3. Reads the response body as bytes
|
||||
//
|
||||
// This function is used internally by ReadJSON to ensure proper JSON response handling.
|
||||
func readJSON(client Client) RIOE.Kleisli[Requester, []byte] {
|
||||
return F.Flow3(
|
||||
ReadFullResponse(client),
|
||||
RIOE.ChainFirstEitherK(F.Flow2(
|
||||
H.Response,
|
||||
H.ValidateJSONResponse,
|
||||
)),
|
||||
RIOE.Map(H.Body),
|
||||
)
|
||||
}
|
||||
|
||||
// ReadJSON sends an HTTP request, reads the response, and parses it as JSON into type A.
|
||||
// It validates both the HTTP status code and the Content-Type header to ensure the
|
||||
// response is valid JSON before attempting to unmarshal.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The target type to unmarshal the JSON response into
|
||||
//
|
||||
// Parameters:
|
||||
// - client: The HTTP client to use for executing the request
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Requester and returns a ReaderIOResult[A]
|
||||
// containing the parsed JSON data
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type User struct {
|
||||
// ID int `json:"id"`
|
||||
// Name string `json:"name"`
|
||||
// }
|
||||
//
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/user/1")
|
||||
// readUser := ReadJSON[User](client)
|
||||
// result := readUser(request)(context.Background())()
|
||||
func ReadJSON[A any](client Client) RIOE.Kleisli[Requester, A] {
|
||||
return F.Flow2(
|
||||
readJSON(client),
|
||||
RIOE.ChainEitherK(J.Unmarshal[A]),
|
||||
)
|
||||
}
|
||||
315
v2/context/readerioresult/http/request_test.go
Normal file
315
v2/context/readerioresult/http/request_test.go
Normal file
@@ -0,0 +1,315 @@
|
||||
// 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
H "net/http"
|
||||
|
||||
R "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/errors"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
IOE "github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type PostItem struct {
|
||||
UserID uint `json:"userId"`
|
||||
Id uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
func getTitle(item PostItem) string {
|
||||
return item.Title
|
||||
}
|
||||
|
||||
type simpleRequestBuilder struct {
|
||||
method string
|
||||
url string
|
||||
headers H.Header
|
||||
}
|
||||
|
||||
func requestBuilder() simpleRequestBuilder {
|
||||
return simpleRequestBuilder{method: "GET"}
|
||||
}
|
||||
|
||||
func (b simpleRequestBuilder) WithURL(url string) simpleRequestBuilder {
|
||||
b.url = url
|
||||
return b
|
||||
}
|
||||
|
||||
func (b simpleRequestBuilder) WithHeader(key, value string) simpleRequestBuilder {
|
||||
if b.headers == nil {
|
||||
b.headers = make(H.Header)
|
||||
} else {
|
||||
b.headers = b.headers.Clone()
|
||||
}
|
||||
b.headers.Set(key, value)
|
||||
return b
|
||||
}
|
||||
|
||||
func (b simpleRequestBuilder) Build() R.ReaderIOResult[*H.Request] {
|
||||
return func(ctx context.Context) IOE.IOEither[error, *H.Request] {
|
||||
return IOE.TryCatchError(func() (*H.Request, error) {
|
||||
req, err := H.NewRequestWithContext(ctx, b.method, b.url, nil)
|
||||
if err == nil {
|
||||
req.Header = b.headers
|
||||
}
|
||||
return req, err
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendSingleRequest(t *testing.T) {
|
||||
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
req1 := MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1")
|
||||
|
||||
readItem := ReadJSON[PostItem](client)
|
||||
|
||||
resp1 := readItem(req1)
|
||||
|
||||
resE := resp1(t.Context())()
|
||||
|
||||
fmt.Println(resE)
|
||||
}
|
||||
|
||||
// setHeaderUnsafe updates a header value in a request object by mutating the request object
|
||||
func setHeaderUnsafe(key, value string) func(*H.Request) *H.Request {
|
||||
return func(req *H.Request) *H.Request {
|
||||
req.Header.Set(key, value)
|
||||
return req
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendSingleRequestWithHeaderUnsafe(t *testing.T) {
|
||||
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
// this is not safe from a puristic perspective, because the map call mutates the request object
|
||||
req1 := F.Pipe2(
|
||||
"https://jsonplaceholder.typicode.com/posts/1",
|
||||
MakeGetRequest,
|
||||
R.Map(setHeaderUnsafe("Content-Type", "text/html")),
|
||||
)
|
||||
|
||||
readItem := ReadJSON[PostItem](client)
|
||||
|
||||
resp1 := F.Pipe2(
|
||||
req1,
|
||||
readItem,
|
||||
R.Map(getTitle),
|
||||
)
|
||||
|
||||
res := F.Pipe1(
|
||||
resp1(t.Context())(),
|
||||
E.GetOrElse(errors.ToString),
|
||||
)
|
||||
|
||||
assert.Equal(t, "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", res)
|
||||
}
|
||||
|
||||
func TestSendSingleRequestWithHeaderSafe(t *testing.T) {
|
||||
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
// the request builder assembles config values to construct
|
||||
// the final http request. Each `With` step creates a copy of the settings
|
||||
// so the flow is pure
|
||||
request := requestBuilder().
|
||||
WithURL("https://jsonplaceholder.typicode.com/posts/1").
|
||||
WithHeader("Content-Type", "text/html").
|
||||
Build()
|
||||
|
||||
readItem := ReadJSON[PostItem](client)
|
||||
|
||||
response := F.Pipe2(
|
||||
request,
|
||||
readItem,
|
||||
R.Map(getTitle),
|
||||
)
|
||||
|
||||
res := F.Pipe1(
|
||||
response(t.Context())(),
|
||||
E.GetOrElse(errors.ToString),
|
||||
)
|
||||
|
||||
assert.Equal(t, "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", res)
|
||||
}
|
||||
|
||||
// TestReadAll tests the ReadAll function which reads response as bytes
|
||||
func TestReadAll(t *testing.T) {
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
request := MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1")
|
||||
readBytes := ReadAll(client)
|
||||
|
||||
result := readBytes(request)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
bytes := E.GetOrElse(func(error) []byte { return nil })(result)
|
||||
assert.NotNil(t, bytes, "Expected non-nil bytes")
|
||||
assert.Greater(t, len(bytes), 0, "Expected non-empty byte array")
|
||||
|
||||
// Verify it contains expected JSON content
|
||||
content := string(bytes)
|
||||
assert.Contains(t, content, "userId")
|
||||
assert.Contains(t, content, "title")
|
||||
}
|
||||
|
||||
// TestReadText tests the ReadText function which reads response as string
|
||||
func TestReadText(t *testing.T) {
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
request := MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1")
|
||||
readText := ReadText(client)
|
||||
|
||||
result := readText(request)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
text := E.GetOrElse(func(error) string { return "" })(result)
|
||||
assert.NotEmpty(t, text, "Expected non-empty text")
|
||||
|
||||
// Verify it contains expected JSON content as text
|
||||
assert.Contains(t, text, "userId")
|
||||
assert.Contains(t, text, "title")
|
||||
assert.Contains(t, text, "sunt aut facere")
|
||||
}
|
||||
|
||||
// TestReadJson tests the deprecated ReadJson function
|
||||
func TestReadJson(t *testing.T) {
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
request := MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1")
|
||||
readItem := ReadJson[PostItem](client)
|
||||
|
||||
result := readItem(request)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
item := E.GetOrElse(func(error) PostItem { return PostItem{} })(result)
|
||||
assert.Equal(t, uint(1), item.UserID, "Expected UserID to be 1")
|
||||
assert.Equal(t, uint(1), item.Id, "Expected Id to be 1")
|
||||
assert.NotEmpty(t, item.Title, "Expected non-empty title")
|
||||
assert.NotEmpty(t, item.Body, "Expected non-empty body")
|
||||
}
|
||||
|
||||
// TestReadAllWithInvalidURL tests ReadAll with an invalid URL
|
||||
func TestReadAllWithInvalidURL(t *testing.T) {
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
request := MakeGetRequest("http://invalid-domain-that-does-not-exist-12345.com")
|
||||
readBytes := ReadAll(client)
|
||||
|
||||
result := readBytes(request)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsLeft(result), "Expected Left result for invalid URL")
|
||||
}
|
||||
|
||||
// TestReadTextWithInvalidURL tests ReadText with an invalid URL
|
||||
func TestReadTextWithInvalidURL(t *testing.T) {
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
request := MakeGetRequest("http://invalid-domain-that-does-not-exist-12345.com")
|
||||
readText := ReadText(client)
|
||||
|
||||
result := readText(request)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsLeft(result), "Expected Left result for invalid URL")
|
||||
}
|
||||
|
||||
// TestReadJSONWithInvalidURL tests ReadJSON with an invalid URL
|
||||
func TestReadJSONWithInvalidURL(t *testing.T) {
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
request := MakeGetRequest("http://invalid-domain-that-does-not-exist-12345.com")
|
||||
readItem := ReadJSON[PostItem](client)
|
||||
|
||||
result := readItem(request)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsLeft(result), "Expected Left result for invalid URL")
|
||||
}
|
||||
|
||||
// TestReadJSONWithInvalidJSON tests ReadJSON with non-JSON response
|
||||
func TestReadJSONWithInvalidJSON(t *testing.T) {
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
// This URL returns HTML, not JSON
|
||||
request := MakeGetRequest("https://www.google.com")
|
||||
readItem := ReadJSON[PostItem](client)
|
||||
|
||||
result := readItem(request)(t.Context())()
|
||||
|
||||
// Should fail because content-type is not application/json
|
||||
assert.True(t, E.IsLeft(result), "Expected Left result for non-JSON response")
|
||||
}
|
||||
|
||||
// TestMakeClientWithCustomClient tests MakeClient with a custom http.Client
|
||||
func TestMakeClientWithCustomClient(t *testing.T) {
|
||||
customClient := H.DefaultClient
|
||||
|
||||
client := MakeClient(customClient)
|
||||
assert.NotNil(t, client, "Expected non-nil client")
|
||||
|
||||
// Verify it works
|
||||
request := MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1")
|
||||
readItem := ReadJSON[PostItem](client)
|
||||
result := readItem(request)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
}
|
||||
|
||||
// TestReadAllComposition tests composing ReadAll with other operations
|
||||
func TestReadAllComposition(t *testing.T) {
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
request := MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1")
|
||||
|
||||
// Compose ReadAll with a map operation to get byte length
|
||||
readBytes := ReadAll(client)(request)
|
||||
readLength := R.Map(func(bytes []byte) int { return len(bytes) })(readBytes)
|
||||
|
||||
result := readLength(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
length := E.GetOrElse(func(error) int { return 0 })(result)
|
||||
assert.Greater(t, length, 0, "Expected positive byte length")
|
||||
}
|
||||
|
||||
// TestReadTextComposition tests composing ReadText with other operations
|
||||
func TestReadTextComposition(t *testing.T) {
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
request := MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1")
|
||||
|
||||
// Compose ReadText with a map operation to get string length
|
||||
readText := ReadText(client)(request)
|
||||
readLength := R.Map(func(text string) int { return len(text) })(readText)
|
||||
|
||||
result := readLength(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
length := E.GetOrElse(func(error) int { return 0 })(result)
|
||||
assert.Greater(t, length, 0, "Expected positive string length")
|
||||
}
|
||||
@@ -13,96 +13,75 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioeither
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||
)
|
||||
|
||||
type (
|
||||
Monoid[A any] = monoid.Monoid[ReaderIOEither[A]]
|
||||
Monoid[A any] = monoid.Monoid[ReaderIOResult[A]]
|
||||
)
|
||||
|
||||
// ApplicativeMonoid returns a [Monoid] that concatenates [ReaderIOEither] instances via their applicative.
|
||||
// ApplicativeMonoid returns a [Monoid] that concatenates [ReaderIOResult] instances via their applicative.
|
||||
// This uses the default applicative behavior (parallel or sequential based on useParallel flag).
|
||||
//
|
||||
// The monoid combines two ReaderIOEither values by applying the underlying monoid's combine operation
|
||||
// The monoid combines two ReaderIOResult values by applying the underlying monoid's combine operation
|
||||
// to their success values using applicative application.
|
||||
//
|
||||
// Parameters:
|
||||
// - m: The underlying monoid for type A
|
||||
//
|
||||
// Returns a Monoid for ReaderIOEither[A].
|
||||
// Returns a Monoid for ReaderIOResult[A].
|
||||
func ApplicativeMonoid[A any](m monoid.Monoid[A]) Monoid[A] {
|
||||
return monoid.ApplicativeMonoid(
|
||||
Of[A],
|
||||
MonadMap[A, func(A) A],
|
||||
MonadAp[A, A],
|
||||
m,
|
||||
)
|
||||
return RIOR.ApplicativeMonoid[context.Context](m)
|
||||
}
|
||||
|
||||
// ApplicativeMonoidSeq returns a [Monoid] that concatenates [ReaderIOEither] instances via their applicative.
|
||||
// ApplicativeMonoidSeq returns a [Monoid] that concatenates [ReaderIOResult] instances via their applicative.
|
||||
// This explicitly uses sequential execution for combining values.
|
||||
//
|
||||
// Parameters:
|
||||
// - m: The underlying monoid for type A
|
||||
//
|
||||
// Returns a Monoid for ReaderIOEither[A] with sequential execution.
|
||||
// Returns a Monoid for ReaderIOResult[A] with sequential execution.
|
||||
func ApplicativeMonoidSeq[A any](m monoid.Monoid[A]) Monoid[A] {
|
||||
return monoid.ApplicativeMonoid(
|
||||
Of[A],
|
||||
MonadMap[A, func(A) A],
|
||||
MonadApSeq[A, A],
|
||||
m,
|
||||
)
|
||||
return RIOR.ApplicativeMonoidSeq[context.Context](m)
|
||||
}
|
||||
|
||||
// ApplicativeMonoidPar returns a [Monoid] that concatenates [ReaderIOEither] instances via their applicative.
|
||||
// ApplicativeMonoidPar returns a [Monoid] that concatenates [ReaderIOResult] instances via their applicative.
|
||||
// This explicitly uses parallel execution for combining values.
|
||||
//
|
||||
// Parameters:
|
||||
// - m: The underlying monoid for type A
|
||||
//
|
||||
// Returns a Monoid for ReaderIOEither[A] with parallel execution.
|
||||
// Returns a Monoid for ReaderIOResult[A] with parallel execution.
|
||||
func ApplicativeMonoidPar[A any](m monoid.Monoid[A]) Monoid[A] {
|
||||
return monoid.ApplicativeMonoid(
|
||||
Of[A],
|
||||
MonadMap[A, func(A) A],
|
||||
MonadApPar[A, A],
|
||||
m,
|
||||
)
|
||||
return RIOR.ApplicativeMonoidPar[context.Context](m)
|
||||
}
|
||||
|
||||
// AlternativeMonoid is the alternative [Monoid] for [ReaderIOEither].
|
||||
// This combines ReaderIOEither values using the alternative semantics,
|
||||
// AlternativeMonoid is the alternative [Monoid] for [ReaderIOResult].
|
||||
// This combines ReaderIOResult values using the alternative semantics,
|
||||
// where the second value is only evaluated if the first fails.
|
||||
//
|
||||
// Parameters:
|
||||
// - m: The underlying monoid for type A
|
||||
//
|
||||
// Returns a Monoid for ReaderIOEither[A] with alternative semantics.
|
||||
// Returns a Monoid for ReaderIOResult[A] with alternative semantics.
|
||||
func AlternativeMonoid[A any](m monoid.Monoid[A]) Monoid[A] {
|
||||
return monoid.AlternativeMonoid(
|
||||
Of[A],
|
||||
MonadMap[A, func(A) A],
|
||||
MonadAp[A, A],
|
||||
MonadAlt[A],
|
||||
m,
|
||||
)
|
||||
return RIOR.AlternativeMonoid[context.Context](m)
|
||||
}
|
||||
|
||||
// AltMonoid is the alternative [Monoid] for a [ReaderIOEither].
|
||||
// AltMonoid is the alternative [Monoid] for a [ReaderIOResult].
|
||||
// This creates a monoid where the empty value is provided lazily,
|
||||
// and combination uses the Alt operation (try first, fallback to second on failure).
|
||||
//
|
||||
// Parameters:
|
||||
// - zero: Lazy computation that provides the empty/identity value
|
||||
//
|
||||
// Returns a Monoid for ReaderIOEither[A] with Alt-based combination.
|
||||
func AltMonoid[A any](zero Lazy[ReaderIOEither[A]]) Monoid[A] {
|
||||
return monoid.AltMonoid(
|
||||
zero,
|
||||
MonadAlt[A],
|
||||
)
|
||||
// Returns a Monoid for ReaderIOResult[A] with Alt-based combination.
|
||||
func AltMonoid[A any](zero Lazy[ReaderIOResult[A]]) Monoid[A] {
|
||||
return RIOR.AltMonoid(zero)
|
||||
}
|
||||
960
v2/context/readerioresult/reader.go
Normal file
960
v2/context/readerioresult/reader.go
Normal file
@@ -0,0 +1,960 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/readerresult"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/errors"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/readeroption"
|
||||
)
|
||||
|
||||
const (
|
||||
// useParallel is the feature flag to control if we use the parallel or the sequential implementation of ap
|
||||
useParallel = true
|
||||
)
|
||||
|
||||
// FromEither converts an [Either] into a [ReaderIOResult].
|
||||
// The resulting computation ignores the context and immediately returns the Either value.
|
||||
//
|
||||
// Parameters:
|
||||
// - e: The Either value to lift into ReaderIOResult
|
||||
//
|
||||
// Returns a ReaderIOResult that produces the given Either value.
|
||||
//
|
||||
//go:inline
|
||||
func FromEither[A any](e Either[A]) ReaderIOResult[A] {
|
||||
return RIOR.FromEither[context.Context](e)
|
||||
}
|
||||
|
||||
// FromEither converts an [Either] into a [ReaderIOResult].
|
||||
// The resulting computation ignores the context and immediately returns the Either value.
|
||||
//
|
||||
// Parameters:
|
||||
// - e: The Either value to lift into ReaderIOResult
|
||||
//
|
||||
// Returns a ReaderIOResult that produces the given Either value.
|
||||
//
|
||||
//go:inline
|
||||
func FromResult[A any](e Result[A]) ReaderIOResult[A] {
|
||||
return RIOR.FromEither[context.Context](e)
|
||||
}
|
||||
|
||||
// Left creates a [ReaderIOResult] that represents a failed computation with the given error.
|
||||
//
|
||||
// Parameters:
|
||||
// - l: The error value
|
||||
//
|
||||
// Returns a ReaderIOResult that always fails with the given error.
|
||||
func Left[A any](l error) ReaderIOResult[A] {
|
||||
return RIOR.Left[context.Context, A](l)
|
||||
}
|
||||
|
||||
// Right creates a [ReaderIOResult] that represents a successful computation with the given value.
|
||||
//
|
||||
// Parameters:
|
||||
// - r: The success value
|
||||
//
|
||||
// Returns a ReaderIOResult that always succeeds with the given value.
|
||||
//
|
||||
//go:inline
|
||||
func Right[A any](r A) ReaderIOResult[A] {
|
||||
return RIOR.Right[context.Context](r)
|
||||
}
|
||||
|
||||
// MonadMap transforms the success value of a [ReaderIOResult] using the provided function.
|
||||
// If the computation fails, the error is propagated unchanged.
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: The ReaderIOResult to transform
|
||||
// - f: The transformation function
|
||||
//
|
||||
// Returns a new ReaderIOResult with the transformed value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadMap[A, B any](fa ReaderIOResult[A], f func(A) B) ReaderIOResult[B] {
|
||||
return RIOR.MonadMap(fa, f)
|
||||
}
|
||||
|
||||
// Map transforms the success value of a [ReaderIOResult] using the provided function.
|
||||
// This is the curried version of [MonadMap], useful for composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: The transformation function
|
||||
//
|
||||
// Returns a function that transforms a ReaderIOResult.
|
||||
//
|
||||
//go:inline
|
||||
func Map[A, B any](f func(A) B) Operator[A, B] {
|
||||
return RIOR.Map[context.Context](f)
|
||||
}
|
||||
|
||||
// MonadMapTo replaces the success value of a [ReaderIOResult] with a constant value.
|
||||
// If the computation fails, the error is propagated unchanged.
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: The ReaderIOResult to transform
|
||||
// - b: The constant value to use
|
||||
//
|
||||
// Returns a new ReaderIOResult with the constant value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadMapTo[A, B any](fa ReaderIOResult[A], b B) ReaderIOResult[B] {
|
||||
return RIOR.MonadMapTo(fa, b)
|
||||
}
|
||||
|
||||
// MapTo replaces the success value of a [ReaderIOResult] with a constant value.
|
||||
// This is the curried version of [MonadMapTo].
|
||||
//
|
||||
// Parameters:
|
||||
// - b: The constant value to use
|
||||
//
|
||||
// Returns a function that transforms a ReaderIOResult.
|
||||
//
|
||||
//go:inline
|
||||
func MapTo[A, B any](b B) Operator[A, B] {
|
||||
return RIOR.MapTo[context.Context, A](b)
|
||||
}
|
||||
|
||||
// MonadChain sequences two [ReaderIOResult] computations, where the second depends on the result of the first.
|
||||
// If the first computation fails, the second is not executed.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The first ReaderIOResult
|
||||
// - f: Function that produces the second ReaderIOResult based on the first's result
|
||||
//
|
||||
// Returns a new ReaderIOResult representing the sequenced computation.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChain[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[B] {
|
||||
return RIOR.MonadChain(ma, f)
|
||||
}
|
||||
|
||||
// Chain sequences two [ReaderIOResult] computations, where the second depends on the result of the first.
|
||||
// This is the curried version of [MonadChain], useful for composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces the second ReaderIOResult based on the first's result
|
||||
//
|
||||
// Returns a function that sequences ReaderIOResult computations.
|
||||
//
|
||||
//go:inline
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||
return RIOR.Chain(f)
|
||||
}
|
||||
|
||||
// MonadChainFirst sequences two [ReaderIOResult] computations but returns the result of the first.
|
||||
// The second computation is executed for its side effects only.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The first ReaderIOResult
|
||||
// - f: Function that produces the second ReaderIOResult
|
||||
//
|
||||
// Returns a ReaderIOResult with the result of the first computation.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirst[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A] {
|
||||
return RIOR.MonadChainFirst(ma, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadTap[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A] {
|
||||
return RIOR.MonadTap(ma, f)
|
||||
}
|
||||
|
||||
// ChainFirst sequences two [ReaderIOResult] computations but returns the result of the first.
|
||||
// This is the curried version of [MonadChainFirst].
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces the second ReaderIOResult
|
||||
//
|
||||
// Returns a function that sequences ReaderIOResult computations.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
||||
return RIOR.ChainFirst(f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Tap[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
||||
return RIOR.Tap(f)
|
||||
}
|
||||
|
||||
// Of creates a [ReaderIOResult] that always succeeds with the given value.
|
||||
// This is the same as [Right] and represents the monadic return operation.
|
||||
//
|
||||
// Parameters:
|
||||
// - a: The value to wrap
|
||||
//
|
||||
// Returns a ReaderIOResult that always succeeds with the given value.
|
||||
//
|
||||
//go:inline
|
||||
func Of[A any](a A) ReaderIOResult[A] {
|
||||
return RIOR.Of[context.Context](a)
|
||||
}
|
||||
|
||||
func withCancelCauseFunc[A any](cancel context.CancelCauseFunc, ma IOResult[A]) IOResult[A] {
|
||||
return function.Pipe3(
|
||||
ma,
|
||||
ioresult.Swap[A],
|
||||
ioeither.ChainFirstIOK[A](func(err error) func() any {
|
||||
return io.FromImpure(func() { cancel(err) })
|
||||
}),
|
||||
ioeither.Swap[A],
|
||||
)
|
||||
}
|
||||
|
||||
// MonadApPar implements parallel applicative application for [ReaderIOResult].
|
||||
// It executes both computations in parallel and creates a sub-context that will be canceled
|
||||
// if either operation fails. This provides automatic cancellation propagation.
|
||||
//
|
||||
// Parameters:
|
||||
// - fab: ReaderIOResult containing a function
|
||||
// - fa: ReaderIOResult containing a value
|
||||
//
|
||||
// Returns a ReaderIOResult with the function applied to the value.
|
||||
func MonadApPar[B, A any](fab ReaderIOResult[func(A) B], fa ReaderIOResult[A]) ReaderIOResult[B] {
|
||||
// context sensitive input
|
||||
cfab := WithContext(fab)
|
||||
cfa := WithContext(fa)
|
||||
|
||||
return func(ctx context.Context) IOResult[B] {
|
||||
// quick check for cancellation
|
||||
if err := context.Cause(ctx); err != nil {
|
||||
return ioeither.Left[B](err)
|
||||
}
|
||||
|
||||
return func() Result[B] {
|
||||
// quick check for cancellation
|
||||
if err := context.Cause(ctx); err != nil {
|
||||
return either.Left[B](err)
|
||||
}
|
||||
|
||||
// create sub-contexts for fa and fab, so they can cancel one other
|
||||
ctxSub, cancelSub := context.WithCancelCause(ctx)
|
||||
defer cancelSub(nil) // cancel has to be called in all paths
|
||||
|
||||
fabIOE := withCancelCauseFunc(cancelSub, cfab(ctxSub))
|
||||
faIOE := withCancelCauseFunc(cancelSub, cfa(ctxSub))
|
||||
|
||||
return ioresult.MonadApPar(fabIOE, faIOE)()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MonadAp implements applicative application for [ReaderIOResult].
|
||||
// By default, it uses parallel execution ([MonadApPar]) but can be configured to use
|
||||
// sequential execution ([MonadApSeq]) via the useParallel constant.
|
||||
//
|
||||
// Parameters:
|
||||
// - fab: ReaderIOResult containing a function
|
||||
// - fa: ReaderIOResult containing a value
|
||||
//
|
||||
// Returns a ReaderIOResult with the function applied to the value.
|
||||
func MonadAp[B, A any](fab ReaderIOResult[func(A) B], fa ReaderIOResult[A]) ReaderIOResult[B] {
|
||||
// dispatch to the configured version
|
||||
if useParallel {
|
||||
return MonadApPar(fab, fa)
|
||||
}
|
||||
return MonadApSeq(fab, fa)
|
||||
}
|
||||
|
||||
// MonadApSeq implements sequential applicative application for [ReaderIOResult].
|
||||
// It executes the function computation first, then the value computation.
|
||||
//
|
||||
// Parameters:
|
||||
// - fab: ReaderIOResult containing a function
|
||||
// - fa: ReaderIOResult containing a value
|
||||
//
|
||||
// Returns a ReaderIOResult with the function applied to the value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadApSeq[B, A any](fab ReaderIOResult[func(A) B], fa ReaderIOResult[A]) ReaderIOResult[B] {
|
||||
return RIOR.MonadApSeq(fab, fa)
|
||||
}
|
||||
|
||||
// Ap applies a function wrapped in a [ReaderIOResult] to a value wrapped in a ReaderIOResult.
|
||||
// This is the curried version of [MonadAp], using the default execution mode.
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: ReaderIOResult containing a value
|
||||
//
|
||||
// Returns a function that applies a ReaderIOResult function to the value.
|
||||
//
|
||||
//go:inline
|
||||
func Ap[B, A any](fa ReaderIOResult[A]) Operator[func(A) B, B] {
|
||||
return function.Bind2nd(MonadAp[B, A], fa)
|
||||
}
|
||||
|
||||
// ApSeq applies a function wrapped in a [ReaderIOResult] to a value sequentially.
|
||||
// This is the curried version of [MonadApSeq].
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: ReaderIOResult containing a value
|
||||
//
|
||||
// Returns a function that applies a ReaderIOResult function to the value sequentially.
|
||||
//
|
||||
//go:inline
|
||||
func ApSeq[B, A any](fa ReaderIOResult[A]) Operator[func(A) B, B] {
|
||||
return function.Bind2nd(MonadApSeq[B, A], fa)
|
||||
}
|
||||
|
||||
// ApPar applies a function wrapped in a [ReaderIOResult] to a value in parallel.
|
||||
// This is the curried version of [MonadApPar].
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: ReaderIOResult containing a value
|
||||
//
|
||||
// Returns a function that applies a ReaderIOResult function to the value in parallel.
|
||||
//
|
||||
//go:inline
|
||||
func ApPar[B, A any](fa ReaderIOResult[A]) Operator[func(A) B, B] {
|
||||
return function.Bind2nd(MonadApPar[B, A], fa)
|
||||
}
|
||||
|
||||
// FromPredicate creates a [ReaderIOResult] from a predicate function.
|
||||
// If the predicate returns true, the value is wrapped in Right; otherwise, Left with the error from onFalse.
|
||||
//
|
||||
// Parameters:
|
||||
// - pred: Predicate function to test the value
|
||||
// - onFalse: Function to generate an error when predicate fails
|
||||
//
|
||||
// Returns a function that converts a value to ReaderIOResult based on the predicate.
|
||||
//
|
||||
//go:inline
|
||||
func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) Kleisli[A, A] {
|
||||
return RIOR.FromPredicate[context.Context](pred, onFalse)
|
||||
}
|
||||
|
||||
// OrElse provides an alternative [ReaderIOResult] computation if the first one fails.
|
||||
// The alternative is only executed if the first computation results in a Left (error).
|
||||
//
|
||||
// Parameters:
|
||||
// - onLeft: Function that produces an alternative ReaderIOResult from the error
|
||||
//
|
||||
// Returns a function that provides fallback behavior for failed computations.
|
||||
//
|
||||
//go:inline
|
||||
func OrElse[A any](onLeft Kleisli[error, A]) Operator[A, A] {
|
||||
return RIOR.OrElse(onLeft)
|
||||
}
|
||||
|
||||
// Ask returns a [ReaderIOResult] that provides access to the context.
|
||||
// This is useful for accessing the [context.Context] within a computation.
|
||||
//
|
||||
// Returns a ReaderIOResult that produces the context.
|
||||
//
|
||||
//go:inline
|
||||
func Ask() ReaderIOResult[context.Context] {
|
||||
return RIOR.Ask[context.Context]()
|
||||
}
|
||||
|
||||
// MonadChainEitherK chains a function that returns an [Either] into a [ReaderIOResult] computation.
|
||||
// This is useful for integrating pure Either-returning functions into ReaderIOResult workflows.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The ReaderIOResult to chain from
|
||||
// - f: Function that produces an Either
|
||||
//
|
||||
// Returns a new ReaderIOResult with the chained computation.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B]) ReaderIOResult[B] {
|
||||
return RIOR.MonadChainEitherK(ma, f)
|
||||
}
|
||||
|
||||
// ChainEitherK chains a function that returns an [Either] into a [ReaderIOResult] computation.
|
||||
// This is the curried version of [MonadChainEitherK].
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces an Either
|
||||
//
|
||||
// Returns a function that chains the Either-returning function.
|
||||
//
|
||||
//go:inline
|
||||
func ChainEitherK[A, B any](f func(A) Either[B]) Operator[A, B] {
|
||||
return RIOR.ChainEitherK[context.Context](f)
|
||||
}
|
||||
|
||||
// MonadChainFirstEitherK chains a function that returns an [Either] but keeps the original value.
|
||||
// The Either-returning function is executed for its validation/side effects only.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The ReaderIOResult to chain from
|
||||
// - f: Function that produces an Either
|
||||
//
|
||||
// Returns a ReaderIOResult with the original value if both computations succeed.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B]) ReaderIOResult[A] {
|
||||
return RIOR.MonadChainFirstEitherK(ma, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadTapEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B]) ReaderIOResult[A] {
|
||||
return RIOR.MonadTapEitherK(ma, f)
|
||||
}
|
||||
|
||||
// ChainFirstEitherK chains a function that returns an [Either] but keeps the original value.
|
||||
// This is the curried version of [MonadChainFirstEitherK].
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces an Either
|
||||
//
|
||||
// Returns a function that chains the Either-returning function.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstEitherK[A, B any](f func(A) Either[B]) Operator[A, A] {
|
||||
return RIOR.ChainFirstEitherK[context.Context](f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TapEitherK[A, B any](f func(A) Either[B]) Operator[A, A] {
|
||||
return RIOR.TapEitherK[context.Context](f)
|
||||
}
|
||||
|
||||
// ChainOptionK chains a function that returns an [Option] into a [ReaderIOResult] computation.
|
||||
// If the Option is None, the provided error function is called.
|
||||
//
|
||||
// Parameters:
|
||||
// - onNone: Function to generate an error when Option is None
|
||||
//
|
||||
// Returns a function that chains Option-returning functions into ReaderIOResult.
|
||||
//
|
||||
//go:inline
|
||||
func ChainOptionK[A, B any](onNone func() error) func(func(A) Option[B]) Operator[A, B] {
|
||||
return RIOR.ChainOptionK[context.Context, A, B](onNone)
|
||||
}
|
||||
|
||||
// FromIOEither converts an [IOResult] into a [ReaderIOResult].
|
||||
// The resulting computation ignores the context.
|
||||
//
|
||||
// Parameters:
|
||||
// - t: The IOResult to convert
|
||||
//
|
||||
// Returns a ReaderIOResult that executes the IOResult.
|
||||
//
|
||||
//go:inline
|
||||
func FromIOEither[A any](t IOResult[A]) ReaderIOResult[A] {
|
||||
return RIOR.FromIOEither[context.Context](t)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FromIOResult[A any](t IOResult[A]) ReaderIOResult[A] {
|
||||
return RIOR.FromIOResult[context.Context](t)
|
||||
}
|
||||
|
||||
// FromIO converts an [IO] into a [ReaderIOResult].
|
||||
// The IO computation always succeeds, so it's wrapped in Right.
|
||||
//
|
||||
// Parameters:
|
||||
// - t: The IO to convert
|
||||
//
|
||||
// Returns a ReaderIOResult that executes the IO and wraps the result in Right.
|
||||
//
|
||||
//go:inline
|
||||
func FromIO[A any](t IO[A]) ReaderIOResult[A] {
|
||||
return RIOR.FromIO[context.Context](t)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FromReader[A any](t Reader[context.Context, A]) ReaderIOResult[A] {
|
||||
return RIOR.FromReader(t)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FromReaderIO[A any](t ReaderIO[A]) ReaderIOResult[A] {
|
||||
return RIOR.FromReaderIO(t)
|
||||
}
|
||||
|
||||
// FromLazy converts a [Lazy] computation into a [ReaderIOResult].
|
||||
// The Lazy computation always succeeds, so it's wrapped in Right.
|
||||
// This is an alias for [FromIO] since Lazy and IO have the same structure.
|
||||
//
|
||||
// Parameters:
|
||||
// - t: The Lazy computation to convert
|
||||
//
|
||||
// Returns a ReaderIOResult that executes the Lazy computation and wraps the result in Right.
|
||||
//
|
||||
//go:inline
|
||||
func FromLazy[A any](t Lazy[A]) ReaderIOResult[A] {
|
||||
return RIOR.FromIO[context.Context](t)
|
||||
}
|
||||
|
||||
// Never returns a [ReaderIOResult] that blocks indefinitely until the context is canceled.
|
||||
// This is useful for creating computations that wait for external cancellation signals.
|
||||
//
|
||||
// Returns a ReaderIOResult that waits for context cancellation and returns the cancellation error.
|
||||
func Never[A any]() ReaderIOResult[A] {
|
||||
return func(ctx context.Context) IOResult[A] {
|
||||
return func() Either[A] {
|
||||
<-ctx.Done()
|
||||
return either.Left[A](context.Cause(ctx))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MonadChainIOK chains a function that returns an [IO] into a [ReaderIOResult] computation.
|
||||
// The IO computation always succeeds, so it's wrapped in Right.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The ReaderIOResult to chain from
|
||||
// - f: Function that produces an IO
|
||||
//
|
||||
// Returns a new ReaderIOResult with the chained IO computation.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderIOResult[B] {
|
||||
return RIOR.MonadChainIOK(ma, f)
|
||||
}
|
||||
|
||||
// ChainIOK chains a function that returns an [IO] into a [ReaderIOResult] computation.
|
||||
// This is the curried version of [MonadChainIOK].
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces an IO
|
||||
//
|
||||
// Returns a function that chains the IO-returning function.
|
||||
//
|
||||
//go:inline
|
||||
func ChainIOK[A, B any](f func(A) IO[B]) Operator[A, B] {
|
||||
return RIOR.ChainIOK[context.Context](f)
|
||||
}
|
||||
|
||||
// MonadChainFirstIOK chains a function that returns an [IO] but keeps the original value.
|
||||
// The IO computation is executed for its side effects only.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The ReaderIOResult to chain from
|
||||
// - f: Function that produces an IO
|
||||
//
|
||||
// Returns a ReaderIOResult with the original value after executing the IO.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderIOResult[A] {
|
||||
return RIOR.MonadChainFirstIOK(ma, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadTapIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderIOResult[A] {
|
||||
return RIOR.MonadTapIOK(ma, f)
|
||||
}
|
||||
|
||||
// ChainFirstIOK chains a function that returns an [IO] but keeps the original value.
|
||||
// This is the curried version of [MonadChainFirstIOK].
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces an IO
|
||||
//
|
||||
// Returns a function that chains the IO-returning function.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
|
||||
return RIOR.ChainFirstIOK[context.Context](f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TapIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
|
||||
return RIOR.TapIOK[context.Context](f)
|
||||
}
|
||||
|
||||
// ChainIOEitherK chains a function that returns an [IOResult] into a [ReaderIOResult] computation.
|
||||
// This is useful for integrating IOResult-returning functions into ReaderIOResult workflows.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that produces an IOResult
|
||||
//
|
||||
// Returns a function that chains the IOResult-returning function.
|
||||
//
|
||||
//go:inline
|
||||
func ChainIOEitherK[A, B any](f func(A) IOResult[B]) Operator[A, B] {
|
||||
return RIOR.ChainIOEitherK[context.Context](f)
|
||||
}
|
||||
|
||||
// Delay creates an operation that delays execution by the specified duration.
|
||||
// The computation waits for either the delay to expire or the context to be canceled.
|
||||
//
|
||||
// Parameters:
|
||||
// - delay: The duration to wait before executing the computation
|
||||
//
|
||||
// Returns a function that delays a ReaderIOResult computation.
|
||||
func Delay[A any](delay time.Duration) Operator[A, A] {
|
||||
return func(ma ReaderIOResult[A]) ReaderIOResult[A] {
|
||||
return func(ctx context.Context) IOResult[A] {
|
||||
return func() Either[A] {
|
||||
// manage the timeout
|
||||
timeoutCtx, cancelTimeout := context.WithTimeout(ctx, delay)
|
||||
defer cancelTimeout()
|
||||
// whatever comes first
|
||||
select {
|
||||
case <-timeoutCtx.Done():
|
||||
return ma(ctx)()
|
||||
case <-ctx.Done():
|
||||
return either.Left[A](context.Cause(ctx))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Timer returns the current time after waiting for the specified delay.
|
||||
// This is useful for creating time-based computations.
|
||||
//
|
||||
// Parameters:
|
||||
// - delay: The duration to wait before returning the time
|
||||
//
|
||||
// Returns a ReaderIOResult that produces the current time after the delay.
|
||||
func Timer(delay time.Duration) ReaderIOResult[time.Time] {
|
||||
return function.Pipe2(
|
||||
io.Now,
|
||||
FromIO[time.Time],
|
||||
Delay[time.Time](delay),
|
||||
)
|
||||
}
|
||||
|
||||
// Defer creates a [ReaderIOResult] by lazily generating a new computation each time it's executed.
|
||||
// This is useful for creating computations that should be re-evaluated on each execution.
|
||||
//
|
||||
// Parameters:
|
||||
// - gen: Lazy generator function that produces a ReaderIOResult
|
||||
//
|
||||
// Returns a ReaderIOResult that generates a fresh computation on each execution.
|
||||
//
|
||||
//go:inline
|
||||
func Defer[A any](gen Lazy[ReaderIOResult[A]]) ReaderIOResult[A] {
|
||||
return RIOR.Defer(gen)
|
||||
}
|
||||
|
||||
// TryCatch wraps a function that returns a tuple (value) into a [ReaderIOResult].
|
||||
// This is the standard way to convert Go error-returning functions into ReaderIOResult.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function that takes a context and returns a function producing (value)
|
||||
//
|
||||
// Returns a ReaderIOResult that wraps the error-returning function.
|
||||
//
|
||||
//go:inline
|
||||
func TryCatch[A any](f func(context.Context) func() (A, error)) ReaderIOResult[A] {
|
||||
return RIOR.TryCatch(f, errors.Identity)
|
||||
}
|
||||
|
||||
// MonadAlt provides an alternative [ReaderIOResult] if the first one fails.
|
||||
// The alternative is lazily evaluated only if needed.
|
||||
//
|
||||
// Parameters:
|
||||
// - first: The primary ReaderIOResult to try
|
||||
// - second: Lazy alternative ReaderIOResult to use if first fails
|
||||
//
|
||||
// Returns a ReaderIOResult that tries the first, then the second if first fails.
|
||||
//
|
||||
//go:inline
|
||||
func MonadAlt[A any](first ReaderIOResult[A], second Lazy[ReaderIOResult[A]]) ReaderIOResult[A] {
|
||||
return RIOR.MonadAlt(first, second)
|
||||
}
|
||||
|
||||
// Alt provides an alternative [ReaderIOResult] if the first one fails.
|
||||
// This is the curried version of [MonadAlt].
|
||||
//
|
||||
// Parameters:
|
||||
// - second: Lazy alternative ReaderIOResult to use if first fails
|
||||
//
|
||||
// Returns a function that provides fallback behavior.
|
||||
//
|
||||
//go:inline
|
||||
func Alt[A any](second Lazy[ReaderIOResult[A]]) Operator[A, A] {
|
||||
return RIOR.Alt(second)
|
||||
}
|
||||
|
||||
// Memoize computes the value of the provided [ReaderIOResult] monad lazily but exactly once.
|
||||
// The context used to compute the value is the context of the first call, so do not use this
|
||||
// method if the value has a functional dependency on the content of the context.
|
||||
//
|
||||
// Parameters:
|
||||
// - rdr: The ReaderIOResult to memoize
|
||||
//
|
||||
// Returns a ReaderIOResult that caches its result after the first execution.
|
||||
//
|
||||
//go:inline
|
||||
func Memoize[A any](rdr ReaderIOResult[A]) ReaderIOResult[A] {
|
||||
return RIOR.Memoize(rdr)
|
||||
}
|
||||
|
||||
// Flatten converts a nested [ReaderIOResult] into a flat [ReaderIOResult].
|
||||
// This is equivalent to [MonadChain] with the identity function.
|
||||
//
|
||||
// Parameters:
|
||||
// - rdr: The nested ReaderIOResult to flatten
|
||||
//
|
||||
// Returns a flattened ReaderIOResult.
|
||||
//
|
||||
//go:inline
|
||||
func Flatten[A any](rdr ReaderIOResult[ReaderIOResult[A]]) ReaderIOResult[A] {
|
||||
return RIOR.Flatten(rdr)
|
||||
}
|
||||
|
||||
// MonadFlap applies a value to a function wrapped in a [ReaderIOResult].
|
||||
// This is the reverse of [MonadAp], useful in certain composition scenarios.
|
||||
//
|
||||
// Parameters:
|
||||
// - fab: ReaderIOResult containing a function
|
||||
// - a: The value to apply to the function
|
||||
//
|
||||
// Returns a ReaderIOResult with the function applied to the value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadFlap[B, A any](fab ReaderIOResult[func(A) B], a A) ReaderIOResult[B] {
|
||||
return RIOR.MonadFlap(fab, a)
|
||||
}
|
||||
|
||||
// Flap applies a value to a function wrapped in a [ReaderIOResult].
|
||||
// This is the curried version of [MonadFlap].
|
||||
//
|
||||
// Parameters:
|
||||
// - a: The value to apply to the function
|
||||
//
|
||||
// Returns a function that applies the value to a ReaderIOResult function.
|
||||
//
|
||||
//go:inline
|
||||
func Flap[B, A any](a A) Operator[func(A) B, B] {
|
||||
return RIOR.Flap[context.Context, B](a)
|
||||
}
|
||||
|
||||
// Fold handles both success and error cases of a [ReaderIOResult] by providing handlers for each.
|
||||
// Both handlers return ReaderIOResult, allowing for further composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - onLeft: Handler for error case
|
||||
// - onRight: Handler for success case
|
||||
//
|
||||
// Returns a function that folds a ReaderIOResult into a new ReaderIOResult.
|
||||
//
|
||||
//go:inline
|
||||
func Fold[A, B any](onLeft Kleisli[error, B], onRight Kleisli[A, B]) Operator[A, B] {
|
||||
return RIOR.Fold(onLeft, onRight)
|
||||
}
|
||||
|
||||
// GetOrElse extracts the value from a [ReaderIOResult], providing a default via a function if it fails.
|
||||
// The result is a [ReaderIO] that always succeeds.
|
||||
//
|
||||
// Parameters:
|
||||
// - onLeft: Function to provide a default value from the error
|
||||
//
|
||||
// Returns a function that converts a ReaderIOResult to a ReaderIO.
|
||||
//
|
||||
//go:inline
|
||||
func GetOrElse[A any](onLeft func(error) ReaderIO[A]) func(ReaderIOResult[A]) ReaderIO[A] {
|
||||
return RIOR.GetOrElse(onLeft)
|
||||
}
|
||||
|
||||
// OrLeft transforms the error of a [ReaderIOResult] using the provided function.
|
||||
// The success value is left unchanged.
|
||||
//
|
||||
// Parameters:
|
||||
// - onLeft: Function to transform the error
|
||||
//
|
||||
// Returns a function that transforms the error of a ReaderIOResult.
|
||||
//
|
||||
//go:inline
|
||||
func OrLeft[A any](onLeft func(error) ReaderIO[error]) Operator[A, A] {
|
||||
return RIOR.OrLeft[A](onLeft)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FromReaderEither[A any](ma ReaderEither[context.Context, error, A]) ReaderIOResult[A] {
|
||||
return RIOR.FromReaderEither(ma)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FromReaderResult[A any](ma ReaderResult[A]) ReaderIOResult[A] {
|
||||
return RIOR.FromReaderEither(ma)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FromReaderOption[A any](onNone func() error) Kleisli[ReaderOption[context.Context, A], A] {
|
||||
return RIOR.FromReaderOption[context.Context, A](onNone)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadChainReaderK[A, B any](ma ReaderIOResult[A], f reader.Kleisli[context.Context, A, B]) ReaderIOResult[B] {
|
||||
return RIOR.MonadChainReaderK(ma, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, B] {
|
||||
return RIOR.ChainReaderK(f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadChainFirstReaderK[A, B any](ma ReaderIOResult[A], f reader.Kleisli[context.Context, A, B]) ReaderIOResult[A] {
|
||||
return RIOR.MonadChainFirstReaderK(ma, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadTapReaderK[A, B any](ma ReaderIOResult[A], f reader.Kleisli[context.Context, A, B]) ReaderIOResult[A] {
|
||||
return RIOR.MonadTapReaderK(ma, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainFirstReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, A] {
|
||||
return RIOR.ChainFirstReaderK(f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TapReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, A] {
|
||||
return RIOR.TapReaderK(f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadChainReaderResultK[A, B any](ma ReaderIOResult[A], f readerresult.Kleisli[A, B]) ReaderIOResult[B] {
|
||||
return RIOR.MonadChainReaderResultK(ma, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainReaderResultK[A, B any](f readerresult.Kleisli[A, B]) Operator[A, B] {
|
||||
return RIOR.ChainReaderResultK(f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadChainFirstReaderResultK[A, B any](ma ReaderIOResult[A], f readerresult.Kleisli[A, B]) ReaderIOResult[A] {
|
||||
return RIOR.MonadChainFirstReaderResultK(ma, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadTapReaderResultK[A, B any](ma ReaderIOResult[A], f readerresult.Kleisli[A, B]) ReaderIOResult[A] {
|
||||
return RIOR.MonadTapReaderResultK(ma, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainFirstReaderResultK[A, B any](f readerresult.Kleisli[A, B]) Operator[A, A] {
|
||||
return RIOR.ChainFirstReaderResultK(f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TapReaderResultK[A, B any](f readerresult.Kleisli[A, B]) Operator[A, A] {
|
||||
return RIOR.TapReaderResultK(f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadChainReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[context.Context, A, B]) ReaderIOResult[B] {
|
||||
return RIOR.MonadChainReaderIOK(ma, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainReaderIOK[A, B any](f readerio.Kleisli[context.Context, A, B]) Operator[A, B] {
|
||||
return RIOR.ChainReaderIOK(f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadChainFirstReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[context.Context, A, B]) ReaderIOResult[A] {
|
||||
return RIOR.MonadChainFirstReaderIOK(ma, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadTapReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[context.Context, A, B]) ReaderIOResult[A] {
|
||||
return RIOR.MonadTapReaderIOK(ma, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainFirstReaderIOK[A, B any](f readerio.Kleisli[context.Context, A, B]) Operator[A, A] {
|
||||
return RIOR.ChainFirstReaderIOK(f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TapReaderIOK[A, B any](f readerio.Kleisli[context.Context, A, B]) Operator[A, A] {
|
||||
return RIOR.TapReaderIOK(f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, B] {
|
||||
return RIOR.ChainReaderOptionK[context.Context, A, B](onNone)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainFirstReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
|
||||
return RIOR.ChainFirstReaderOptionK[context.Context, A, B](onNone)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TapReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
|
||||
return RIOR.TapReaderOptionK[context.Context, A, B](onNone)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Read[A any](r context.Context) func(ReaderIOResult[A]) IOResult[A] {
|
||||
return RIOR.Read[A](r)
|
||||
}
|
||||
|
||||
// MonadChainLeft chains a computation on the left (error) side of a [ReaderIOResult].
|
||||
// If the input is a Left value, it applies the function f to transform the error and potentially
|
||||
// change the error type. If the input is a Right value, it passes through unchanged.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainLeft[A any](fa ReaderIOResult[A], f Kleisli[error, A]) ReaderIOResult[A] {
|
||||
return RIOR.MonadChainLeft(fa, f)
|
||||
}
|
||||
|
||||
// ChainLeft is the curried version of [MonadChainLeft].
|
||||
// It returns a function that chains a computation on the left (error) side of a [ReaderIOResult].
|
||||
//
|
||||
//go:inline
|
||||
func ChainLeft[A any](f Kleisli[error, A]) func(ReaderIOResult[A]) ReaderIOResult[A] {
|
||||
return RIOR.ChainLeft(f)
|
||||
}
|
||||
|
||||
// MonadChainFirstLeft chains a computation on the left (error) side but always returns the original error.
|
||||
// If the input is a Left value, it applies the function f to the error and executes the resulting computation,
|
||||
// but always returns the original Left error regardless of what f returns (Left or Right).
|
||||
// If the input is a Right value, it passes through unchanged without calling f.
|
||||
//
|
||||
// This is useful for side effects on errors (like logging or metrics) where you want to perform an action
|
||||
// when an error occurs but always propagate the original error, ensuring the error path is preserved.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOResult[A] {
|
||||
return RIOR.MonadChainFirstLeft(ma, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadTapLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOResult[A] {
|
||||
return RIOR.MonadTapLeft(ma, f)
|
||||
}
|
||||
|
||||
// ChainFirstLeft is the curried version of [MonadChainFirstLeft].
|
||||
// It returns a function that chains a computation on the left (error) side while always preserving the original error.
|
||||
//
|
||||
// This is particularly useful for adding error handling side effects (like logging, metrics, or notifications)
|
||||
// in a functional pipeline. The original error is always returned regardless of what f returns (Left or Right),
|
||||
// ensuring the error path is preserved.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
|
||||
return RIOR.ChainFirstLeft[A](f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TapLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
|
||||
return RIOR.TapLeft[A](f)
|
||||
}
|
||||
886
v2/context/readerioresult/reader_bench_test.go
Normal file
886
v2/context/readerioresult/reader_bench_test.go
Normal file
@@ -0,0 +1,886 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
IOE "github.com/IBM/fp-go/v2/ioeither"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
var (
|
||||
benchErr = errors.New("benchmark error")
|
||||
benchCtx = context.Background()
|
||||
benchResult Either[int]
|
||||
benchRIOE ReaderIOResult[int]
|
||||
benchInt int
|
||||
)
|
||||
|
||||
// Benchmark core constructors
|
||||
func BenchmarkLeft(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = Left[int](benchErr)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRight(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = Right(42)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkOf(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = Of(42)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFromEither_Right(b *testing.B) {
|
||||
either := E.Right[error](42)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = FromEither(either)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFromEither_Left(b *testing.B) {
|
||||
either := E.Left[int](benchErr)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = FromEither(either)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFromIO(b *testing.B) {
|
||||
io := func() int { return 42 }
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = FromIO(io)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFromIOEither_Right(b *testing.B) {
|
||||
ioe := IOE.Of[error](42)
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = FromIOEither(ioe)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFromIOEither_Left(b *testing.B) {
|
||||
ioe := IOE.Left[int](benchErr)
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = FromIOEither(ioe)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark execution
|
||||
func BenchmarkExecute_Right(b *testing.B) {
|
||||
rioe := Right(42)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResult = rioe(benchCtx)()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExecute_Left(b *testing.B) {
|
||||
rioe := Left[int](benchErr)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResult = rioe(benchCtx)()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExecute_WithContext(b *testing.B) {
|
||||
rioe := Right(42)
|
||||
ctx, cancel := context.WithCancel(benchCtx)
|
||||
defer cancel()
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResult = rioe(ctx)()
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark functor operations
|
||||
func BenchmarkMonadMap_Right(b *testing.B) {
|
||||
rioe := Right(42)
|
||||
mapper := N.Mul(2)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = MonadMap(rioe, mapper)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMonadMap_Left(b *testing.B) {
|
||||
rioe := Left[int](benchErr)
|
||||
mapper := N.Mul(2)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = MonadMap(rioe, mapper)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMap_Right(b *testing.B) {
|
||||
rioe := Right(42)
|
||||
mapper := Map(N.Mul(2))
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = mapper(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMap_Left(b *testing.B) {
|
||||
rioe := Left[int](benchErr)
|
||||
mapper := Map(N.Mul(2))
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = mapper(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMapTo_Right(b *testing.B) {
|
||||
rioe := Right(42)
|
||||
mapper := MapTo[int](99)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = mapper(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark monad operations
|
||||
func BenchmarkMonadChain_Right(b *testing.B) {
|
||||
rioe := Right(42)
|
||||
chainer := func(a int) ReaderIOResult[int] { return Right(a * 2) }
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = MonadChain(rioe, chainer)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMonadChain_Left(b *testing.B) {
|
||||
rioe := Left[int](benchErr)
|
||||
chainer := func(a int) ReaderIOResult[int] { return Right(a * 2) }
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = MonadChain(rioe, chainer)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChain_Right(b *testing.B) {
|
||||
rioe := Right(42)
|
||||
chainer := Chain(func(a int) ReaderIOResult[int] { return Right(a * 2) })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = chainer(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChain_Left(b *testing.B) {
|
||||
rioe := Left[int](benchErr)
|
||||
chainer := Chain(func(a int) ReaderIOResult[int] { return Right(a * 2) })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = chainer(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChainFirst_Right(b *testing.B) {
|
||||
rioe := Right(42)
|
||||
chainer := ChainFirst(func(a int) ReaderIOResult[string] { return Right("logged") })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = chainer(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChainFirst_Left(b *testing.B) {
|
||||
rioe := Left[int](benchErr)
|
||||
chainer := ChainFirst(func(a int) ReaderIOResult[string] { return Right("logged") })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = chainer(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFlatten_Right(b *testing.B) {
|
||||
nested := Right(Right(42))
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = Flatten(nested)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFlatten_Left(b *testing.B) {
|
||||
nested := Left[ReaderIOResult[int]](benchErr)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = Flatten(nested)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark applicative operations
|
||||
func BenchmarkMonadApSeq_RightRight(b *testing.B) {
|
||||
fab := Right(N.Mul(2))
|
||||
fa := Right(42)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = MonadApSeq(fab, fa)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMonadApSeq_RightLeft(b *testing.B) {
|
||||
fab := Right(N.Mul(2))
|
||||
fa := Left[int](benchErr)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = MonadApSeq(fab, fa)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMonadApSeq_LeftRight(b *testing.B) {
|
||||
fab := Left[func(int) int](benchErr)
|
||||
fa := Right(42)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = MonadApSeq(fab, fa)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMonadApPar_RightRight(b *testing.B) {
|
||||
fab := Right(N.Mul(2))
|
||||
fa := Right(42)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = MonadApPar(fab, fa)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMonadApPar_RightLeft(b *testing.B) {
|
||||
fab := Right(N.Mul(2))
|
||||
fa := Left[int](benchErr)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = MonadApPar(fab, fa)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMonadApPar_LeftRight(b *testing.B) {
|
||||
fab := Left[func(int) int](benchErr)
|
||||
fa := Right(42)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = MonadApPar(fab, fa)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark execution of applicative operations
|
||||
func BenchmarkExecuteApSeq_RightRight(b *testing.B) {
|
||||
fab := Right(N.Mul(2))
|
||||
fa := Right(42)
|
||||
rioe := MonadApSeq(fab, fa)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResult = rioe(benchCtx)()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExecuteApPar_RightRight(b *testing.B) {
|
||||
fab := Right(N.Mul(2))
|
||||
fa := Right(42)
|
||||
rioe := MonadApPar(fab, fa)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResult = rioe(benchCtx)()
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark alternative operations
|
||||
func BenchmarkAlt_RightRight(b *testing.B) {
|
||||
rioe := Right(42)
|
||||
alternative := Alt(func() ReaderIOResult[int] { return Right(99) })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = alternative(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAlt_LeftRight(b *testing.B) {
|
||||
rioe := Left[int](benchErr)
|
||||
alternative := Alt(func() ReaderIOResult[int] { return Right(99) })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = alternative(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkOrElse_Right(b *testing.B) {
|
||||
rioe := Right(42)
|
||||
recover := OrElse(func(e error) ReaderIOResult[int] { return Right(0) })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = recover(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkOrElse_Left(b *testing.B) {
|
||||
rioe := Left[int](benchErr)
|
||||
recover := OrElse(func(e error) ReaderIOResult[int] { return Right(0) })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = recover(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark chain operations with different types
|
||||
func BenchmarkChainEitherK_Right(b *testing.B) {
|
||||
rioe := Right(42)
|
||||
chainer := ChainEitherK(func(a int) Either[int] { return E.Right[error](a * 2) })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = chainer(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChainEitherK_Left(b *testing.B) {
|
||||
rioe := Left[int](benchErr)
|
||||
chainer := ChainEitherK(func(a int) Either[int] { return E.Right[error](a * 2) })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = chainer(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChainIOK_Right(b *testing.B) {
|
||||
rioe := Right(42)
|
||||
chainer := ChainIOK(func(a int) func() int { return func() int { return a * 2 } })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = chainer(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChainIOK_Left(b *testing.B) {
|
||||
rioe := Left[int](benchErr)
|
||||
chainer := ChainIOK(func(a int) func() int { return func() int { return a * 2 } })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = chainer(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChainIOEitherK_Right(b *testing.B) {
|
||||
rioe := Right(42)
|
||||
chainer := ChainIOEitherK(func(a int) IOEither[int] { return IOE.Of[error](a * 2) })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = chainer(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChainIOEitherK_Left(b *testing.B) {
|
||||
rioe := Left[int](benchErr)
|
||||
chainer := ChainIOEitherK(func(a int) IOEither[int] { return IOE.Of[error](a * 2) })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = chainer(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark context operations
|
||||
func BenchmarkAsk(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_ = Ask()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDefer(b *testing.B) {
|
||||
gen := func() ReaderIOResult[int] { return Right(42) }
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = Defer(gen)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMemoize(b *testing.B) {
|
||||
rioe := Right(42)
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = Memoize(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark delay operations
|
||||
func BenchmarkDelay_Construction(b *testing.B) {
|
||||
rioe := Right(42)
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = Delay[int](time.Millisecond)(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTimer_Construction(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_ = Timer(time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark TryCatch
|
||||
func BenchmarkTryCatch_Success(b *testing.B) {
|
||||
f := func(ctx context.Context) func() (int, error) {
|
||||
return func() (int, error) { return 42, nil }
|
||||
}
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = TryCatch(f)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTryCatch_Error(b *testing.B) {
|
||||
f := func(ctx context.Context) func() (int, error) {
|
||||
return func() (int, error) { return 0, benchErr }
|
||||
}
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = TryCatch(f)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExecuteTryCatch_Success(b *testing.B) {
|
||||
f := func(ctx context.Context) func() (int, error) {
|
||||
return func() (int, error) { return 42, nil }
|
||||
}
|
||||
rioe := TryCatch(f)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResult = rioe(benchCtx)()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExecuteTryCatch_Error(b *testing.B) {
|
||||
f := func(ctx context.Context) func() (int, error) {
|
||||
return func() (int, error) { return 0, benchErr }
|
||||
}
|
||||
rioe := TryCatch(f)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResult = rioe(benchCtx)()
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark pipeline operations
|
||||
func BenchmarkPipeline_Map_Right(b *testing.B) {
|
||||
rioe := Right(21)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = F.Pipe1(
|
||||
rioe,
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPipeline_Map_Left(b *testing.B) {
|
||||
rioe := Left[int](benchErr)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = F.Pipe1(
|
||||
rioe,
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPipeline_Chain_Right(b *testing.B) {
|
||||
rioe := Right(21)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = F.Pipe1(
|
||||
rioe,
|
||||
Chain(func(x int) ReaderIOResult[int] { return Right(x * 2) }),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPipeline_Chain_Left(b *testing.B) {
|
||||
rioe := Left[int](benchErr)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = F.Pipe1(
|
||||
rioe,
|
||||
Chain(func(x int) ReaderIOResult[int] { return Right(x * 2) }),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPipeline_Complex_Right(b *testing.B) {
|
||||
rioe := Right(10)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = F.Pipe3(
|
||||
rioe,
|
||||
Map(N.Mul(2)),
|
||||
Chain(func(x int) ReaderIOResult[int] { return Right(x + 1) }),
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPipeline_Complex_Left(b *testing.B) {
|
||||
rioe := Left[int](benchErr)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = F.Pipe3(
|
||||
rioe,
|
||||
Map(N.Mul(2)),
|
||||
Chain(func(x int) ReaderIOResult[int] { return Right(x + 1) }),
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExecutePipeline_Complex_Right(b *testing.B) {
|
||||
rioe := F.Pipe3(
|
||||
Right(10),
|
||||
Map(N.Mul(2)),
|
||||
Chain(func(x int) ReaderIOResult[int] { return Right(x + 1) }),
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResult = rioe(benchCtx)()
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark do-notation operations
|
||||
func BenchmarkDo(b *testing.B) {
|
||||
type State struct{ value int }
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_ = Do(State{})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBind_Right(b *testing.B) {
|
||||
type State struct{ value int }
|
||||
initial := Do(State{})
|
||||
binder := Bind(
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State { return State{value: v} }
|
||||
},
|
||||
func(s State) ReaderIOResult[int] {
|
||||
return Right(42)
|
||||
},
|
||||
)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_ = binder(initial)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLet_Right(b *testing.B) {
|
||||
type State struct{ value int }
|
||||
initial := Right(State{value: 10})
|
||||
letter := Let(
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State { return State{value: s.value + v} }
|
||||
},
|
||||
func(s State) int { return 32 },
|
||||
)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_ = letter(initial)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkApS_Right(b *testing.B) {
|
||||
type State struct{ value int }
|
||||
initial := Right(State{value: 10})
|
||||
aps := ApS(
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State { return State{value: v} }
|
||||
},
|
||||
Right(42),
|
||||
)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_ = aps(initial)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark traverse operations
|
||||
func BenchmarkTraverseArray_Empty(b *testing.B) {
|
||||
arr := []int{}
|
||||
traverser := TraverseArray(func(x int) ReaderIOResult[int] {
|
||||
return Right(x * 2)
|
||||
})
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_ = traverser(arr)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTraverseArray_Small(b *testing.B) {
|
||||
arr := []int{1, 2, 3}
|
||||
traverser := TraverseArray(func(x int) ReaderIOResult[int] {
|
||||
return Right(x * 2)
|
||||
})
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_ = traverser(arr)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTraverseArray_Medium(b *testing.B) {
|
||||
arr := make([]int, 10)
|
||||
for i := range arr {
|
||||
arr[i] = i
|
||||
}
|
||||
traverser := TraverseArray(func(x int) ReaderIOResult[int] {
|
||||
return Right(x * 2)
|
||||
})
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_ = traverser(arr)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTraverseArraySeq_Small(b *testing.B) {
|
||||
arr := []int{1, 2, 3}
|
||||
traverser := TraverseArraySeq(func(x int) ReaderIOResult[int] {
|
||||
return Right(x * 2)
|
||||
})
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_ = traverser(arr)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTraverseArrayPar_Small(b *testing.B) {
|
||||
arr := []int{1, 2, 3}
|
||||
traverser := TraverseArrayPar(func(x int) ReaderIOResult[int] {
|
||||
return Right(x * 2)
|
||||
})
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_ = traverser(arr)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSequenceArray_Small(b *testing.B) {
|
||||
arr := []ReaderIOResult[int]{
|
||||
Right(1),
|
||||
Right(2),
|
||||
Right(3),
|
||||
}
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_ = SequenceArray(arr)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExecuteTraverseArray_Small(b *testing.B) {
|
||||
arr := []int{1, 2, 3}
|
||||
rioe := TraverseArray(func(x int) ReaderIOResult[int] {
|
||||
return Right(x * 2)
|
||||
})(arr)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_ = rioe(benchCtx)()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExecuteTraverseArraySeq_Small(b *testing.B) {
|
||||
arr := []int{1, 2, 3}
|
||||
rioe := TraverseArraySeq(func(x int) ReaderIOResult[int] {
|
||||
return Right(x * 2)
|
||||
})(arr)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_ = rioe(benchCtx)()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExecuteTraverseArrayPar_Small(b *testing.B) {
|
||||
arr := []int{1, 2, 3}
|
||||
rioe := TraverseArrayPar(func(x int) ReaderIOResult[int] {
|
||||
return Right(x * 2)
|
||||
})(arr)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_ = rioe(benchCtx)()
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark record operations
|
||||
func BenchmarkTraverseRecord_Small(b *testing.B) {
|
||||
rec := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
traverser := TraverseRecord[string](func(x int) ReaderIOResult[int] {
|
||||
return Right(x * 2)
|
||||
})
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_ = traverser(rec)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSequenceRecord_Small(b *testing.B) {
|
||||
rec := map[string]ReaderIOResult[int]{
|
||||
"a": Right(1),
|
||||
"b": Right(2),
|
||||
"c": Right(3),
|
||||
}
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_ = SequenceRecord(rec)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark resource management
|
||||
func BenchmarkWithResource_Success(b *testing.B) {
|
||||
acquire := Right(42)
|
||||
release := func(int) ReaderIOResult[int] { return Right(0) }
|
||||
body := func(x int) ReaderIOResult[int] { return Right(x * 2) }
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_ = WithResource[int](acquire, release)(body)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExecuteWithResource_Success(b *testing.B) {
|
||||
acquire := Right(42)
|
||||
release := func(int) ReaderIOResult[int] { return Right(0) }
|
||||
body := func(x int) ReaderIOResult[int] { return Right(x * 2) }
|
||||
rioe := WithResource[int](acquire, release)(body)
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResult = rioe(benchCtx)()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExecuteWithResource_ErrorInBody(b *testing.B) {
|
||||
acquire := Right(42)
|
||||
release := func(int) ReaderIOResult[int] { return Right(0) }
|
||||
body := func(x int) ReaderIOResult[int] { return Left[int](benchErr) }
|
||||
rioe := WithResource[int](acquire, release)(body)
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResult = rioe(benchCtx)()
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark context cancellation
|
||||
func BenchmarkExecute_CanceledContext(b *testing.B) {
|
||||
rioe := Right(42)
|
||||
ctx, cancel := context.WithCancel(benchCtx)
|
||||
cancel() // Cancel immediately
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResult = rioe(ctx)()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExecuteApPar_CanceledContext(b *testing.B) {
|
||||
fab := Right(N.Mul(2))
|
||||
fa := Right(42)
|
||||
rioe := MonadApPar(fab, fa)
|
||||
ctx, cancel := context.WithCancel(benchCtx)
|
||||
cancel() // Cancel immediately
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchResult = rioe(ctx)()
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user