mirror of
https://github.com/IBM/fp-go.git
synced 2026-01-29 10:36:04 +02:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da0344f9bd | ||
|
|
cd79dd56b9 | ||
|
|
df07599a9e | ||
|
|
30ad0e4dd8 | ||
|
|
2374d7f1e4 | ||
|
|
eafc008798 | ||
|
|
46bf065e34 | ||
|
|
b4e303423b | ||
|
|
7afc098f58 | ||
|
|
617e43de19 | ||
|
|
0f7a6c0589 | ||
|
|
e7f78e1a33 | ||
|
|
6505ab1791 | ||
|
|
cfa48985ec | ||
|
|
677523b70f | ||
|
|
8243242cf1 |
14
.github/workflows/build.yml
vendored
14
.github/workflows/build.yml
vendored
@@ -28,11 +28,11 @@ jobs:
|
||||
fail-fast: false # Continue with other versions if one fails
|
||||
steps:
|
||||
# full checkout for semantic-release
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go ${{ matrix.go-version }}
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
cache: true # Enable Go module caching
|
||||
@@ -66,11 +66,11 @@ jobs:
|
||||
matrix:
|
||||
go-version: ['1.24.x', '1.25.x']
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go ${{ matrix.go-version }}
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
cache: true # Enable Go module caching
|
||||
@@ -126,17 +126,17 @@ jobs:
|
||||
steps:
|
||||
# full checkout for semantic-release
|
||||
- name: Full checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ env.LATEST_GO_VERSION }}
|
||||
cache: true # Enable Go module caching
|
||||
|
||||
16
go.sum
16
go.sum
@@ -1,7 +1,3 @@
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@@ -10,20 +6,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
|
||||
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8=
|
||||
github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ=
|
||||
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
|
||||
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
||||
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
|
||||
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
||||
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
|
||||
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
"matchDepTypes": [
|
||||
"golang"
|
||||
],
|
||||
"enabled": false
|
||||
"enabled": false,
|
||||
"description": "Disable updates to the go directive in go.mod files - the directive identifies the minimum compatible Go version and should stay as small as possible for maximum compatibility"
|
||||
},
|
||||
{
|
||||
"matchUpdateTypes": [
|
||||
|
||||
@@ -584,5 +584,7 @@ func process(input string) types.Result[types.Option[int]] {
|
||||
|
||||
For more information, see:
|
||||
- [README.md](./README.md) - Overview and quick start
|
||||
- [FUNCTIONAL_IO.md](./FUNCTIONAL_IO.md) - Functional I/O patterns with Context and Reader
|
||||
- [IDIOMATIC_COMPARISON.md](./IDIOMATIC_COMPARISON.md) - Performance comparison between standard and idiomatic packages
|
||||
- [API Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2) - Complete API reference
|
||||
- [Samples](./samples/) - Practical examples
|
||||
829
v2/FUNCTIONAL_IO.md
Normal file
829
v2/FUNCTIONAL_IO.md
Normal file
@@ -0,0 +1,829 @@
|
||||
# Functional I/O in Go: Context, Errors, and the Reader Pattern
|
||||
|
||||
This document explores how functional programming principles apply to I/O operations in Go, comparing traditional imperative approaches with functional patterns using the `context/readerioresult` and `idiomatic/context/readerresult` packages.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Why Context in I/O Operations](#why-context-in-io-operations)
|
||||
- [The Error-Value Tuple Pattern](#the-error-value-tuple-pattern)
|
||||
- [Functional Approach: Reader Pattern](#functional-approach-reader-pattern)
|
||||
- [Benefits of the Functional Approach](#benefits-of-the-functional-approach)
|
||||
- [Side-by-Side Comparison](#side-by-side-comparison)
|
||||
- [Advanced Patterns](#advanced-patterns)
|
||||
- [When to Use Each Approach](#when-to-use-each-approach)
|
||||
|
||||
## Why Context in I/O Operations
|
||||
|
||||
In idiomatic Go, I/O operations conventionally take a `context.Context` as their first parameter:
|
||||
|
||||
```go
|
||||
func QueryDatabase(ctx context.Context, query string) (Result, error)
|
||||
func MakeHTTPRequest(ctx context.Context, url string) (*http.Response, error)
|
||||
func ReadFile(ctx context.Context, path string) ([]byte, error)
|
||||
```
|
||||
|
||||
### The Purpose of Context
|
||||
|
||||
The `context.Context` parameter serves several critical purposes:
|
||||
|
||||
1. **Cancellation Propagation**: Operations can be cancelled when the context is cancelled
|
||||
2. **Deadline Management**: Operations respect timeouts and deadlines
|
||||
3. **Request-Scoped Values**: Carry request metadata (trace IDs, user info, etc.)
|
||||
4. **Resource Cleanup**: Signal to release resources when work is no longer needed
|
||||
|
||||
### Why Context Matters for I/O
|
||||
|
||||
I/O operations are inherently **effectful** - they interact with the outside world:
|
||||
- Reading from disk, network, or database
|
||||
- Writing to external systems
|
||||
- Generating random numbers
|
||||
- Reading the current time
|
||||
|
||||
These operations can:
|
||||
- **Take time**: Network calls may be slow
|
||||
- **Fail**: Connections drop, files don't exist
|
||||
- **Block**: Waiting for external resources
|
||||
- **Need cancellation**: User navigates away, request times out
|
||||
|
||||
Context provides a standard mechanism to control these operations across your entire application.
|
||||
|
||||
## The Error-Value Tuple Pattern
|
||||
|
||||
### Why Operations Must Return Errors
|
||||
|
||||
In Go, I/O operations return `(value, error)` tuples because:
|
||||
|
||||
1. **Context can be cancelled**: Even if the operation would succeed, cancellation must be represented
|
||||
2. **External systems fail**: Networks fail, files are missing, permissions are denied
|
||||
3. **Resources are exhausted**: Out of memory, disk full, connection pool exhausted
|
||||
4. **Timeouts occur**: Operations exceed their deadline
|
||||
|
||||
**There cannot be I/O operations without error handling** because the context itself introduces a failure mode (cancellation) that must be represented in the return type.
|
||||
|
||||
### Traditional Go Pattern
|
||||
|
||||
```go
|
||||
func ProcessUser(ctx context.Context, userID int) (User, error) {
|
||||
// Check context before starting
|
||||
if err := ctx.Err(); err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
// Fetch user from database
|
||||
user, err := db.QueryUser(ctx, userID)
|
||||
if err != nil {
|
||||
return User{}, fmt.Errorf("query user: %w", err)
|
||||
}
|
||||
|
||||
// Validate user
|
||||
if user.Age < 18 {
|
||||
return User{}, errors.New("user too young")
|
||||
}
|
||||
|
||||
// Fetch user's posts
|
||||
posts, err := db.QueryPosts(ctx, user.ID)
|
||||
if err != nil {
|
||||
return User{}, fmt.Errorf("query posts: %w", err)
|
||||
}
|
||||
|
||||
user.Posts = posts
|
||||
return user, nil
|
||||
}
|
||||
```
|
||||
|
||||
**Characteristics:**
|
||||
- Explicit error checking at each step
|
||||
- Manual error wrapping and propagation
|
||||
- Context checked manually
|
||||
- Imperative control flow
|
||||
- Error handling mixed with business logic
|
||||
|
||||
## Functional Approach: Reader Pattern
|
||||
|
||||
### The Core Insight
|
||||
|
||||
In functional programming, we separate **what to compute** from **how to execute it**. Instead of functions that perform I/O directly, we create functions that **return descriptions of I/O operations**.
|
||||
|
||||
### Key Type: ReaderIOResult
|
||||
|
||||
```go
|
||||
// A function that takes a context and returns a value or error
|
||||
type ReaderIOResult[A any] = func(context.Context) (A, error)
|
||||
```
|
||||
|
||||
This type represents:
|
||||
- **Reader**: Depends on an environment (context.Context)
|
||||
- **IO**: Performs side effects (I/O operations)
|
||||
- **Result**: Can fail with an error
|
||||
|
||||
### Why This Is Better
|
||||
|
||||
The functional approach **carries the I/O aspect as the return value, not on the input**:
|
||||
|
||||
```go
|
||||
// Traditional: I/O is implicit in the function execution
|
||||
func fetchUser(ctx context.Context, id int) (User, error) {
|
||||
// Performs I/O immediately
|
||||
}
|
||||
|
||||
// Functional: I/O is explicit in the return type
|
||||
func fetchUser(id int) ReaderIOResult[User] {
|
||||
// Returns a description of I/O, doesn't execute yet
|
||||
return func(ctx context.Context) (User, error) {
|
||||
// I/O happens here when the function is called
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key difference**: The functional version is a **curried function** where:
|
||||
1. Business parameters come first: `fetchUser(id)`
|
||||
2. Context comes last: `fetchUser(id)(ctx)`
|
||||
3. The intermediate result is composable: `ReaderIOResult[User]`
|
||||
|
||||
## Benefits of the Functional Approach
|
||||
|
||||
### 1. Separation of Pure and Impure Code
|
||||
|
||||
```go
|
||||
// Pure computation - no I/O, no context needed
|
||||
func validateAge(user User) (User, error) {
|
||||
if user.Age < 18 {
|
||||
return User{}, errors.New("user too young")
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Impure I/O operation - needs context
|
||||
func fetchUser(id int) ReaderIOResult[User] {
|
||||
return func(ctx context.Context) (User, error) {
|
||||
return db.QueryUser(ctx, id)
|
||||
}
|
||||
}
|
||||
|
||||
// Compose them - pure logic lifted into ReaderIOResult
|
||||
pipeline := F.Pipe2(
|
||||
fetchUser(42), // ReaderIOResult[User]
|
||||
readerioresult.ChainEitherK(validateAge), // Lift pure function
|
||||
)
|
||||
|
||||
// Execute when ready
|
||||
user, err := pipeline(ctx)
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Pure functions are easier to test (no mocking needed)
|
||||
- Pure functions are easier to reason about (no side effects)
|
||||
- Clear boundary between logic and I/O
|
||||
- Can test business logic independently
|
||||
|
||||
### 2. Composability
|
||||
|
||||
Functions compose naturally without manual error checking:
|
||||
|
||||
```go
|
||||
// Traditional approach - manual error handling
|
||||
func ProcessUserTraditional(ctx context.Context, userID int) (UserWithPosts, error) {
|
||||
user, err := fetchUser(ctx, userID)
|
||||
if err != nil {
|
||||
return UserWithPosts{}, err
|
||||
}
|
||||
|
||||
validated, err := validateUser(user)
|
||||
if err != nil {
|
||||
return UserWithPosts{}, err
|
||||
}
|
||||
|
||||
posts, err := fetchPosts(ctx, validated.ID)
|
||||
if err != nil {
|
||||
return UserWithPosts{}, err
|
||||
}
|
||||
|
||||
return enrichUser(validated, posts), nil
|
||||
}
|
||||
|
||||
// Functional approach - automatic error propagation
|
||||
func ProcessUserFunctional(userID int) ReaderIOResult[UserWithPosts] {
|
||||
return F.Pipe3(
|
||||
fetchUser(userID),
|
||||
readerioresult.ChainEitherK(validateUser),
|
||||
readerioresult.Chain(func(user User) ReaderIOResult[UserWithPosts] {
|
||||
return F.Pipe2(
|
||||
fetchPosts(user.ID),
|
||||
readerioresult.Map(func(posts []Post) UserWithPosts {
|
||||
return enrichUser(user, posts)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- No manual error checking
|
||||
- Automatic short-circuiting on first error
|
||||
- Clear data flow
|
||||
- Easier to refactor and extend
|
||||
|
||||
### 3. Testability
|
||||
|
||||
```go
|
||||
// Mock I/O operations by providing test implementations
|
||||
func TestProcessUser(t *testing.T) {
|
||||
// Create a mock that returns test data
|
||||
mockFetchUser := func(id int) ReaderIOResult[User] {
|
||||
return func(ctx context.Context) (User, error) {
|
||||
return User{ID: id, Age: 25}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Test with mock - no database needed
|
||||
result, err := mockFetchUser(42)(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 25, result.Age)
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Lazy Evaluation
|
||||
|
||||
Operations are not executed until you provide the context:
|
||||
|
||||
```go
|
||||
// Build the pipeline - no I/O happens yet
|
||||
pipeline := F.Pipe3(
|
||||
fetchUser(42),
|
||||
readerioresult.Map(enrichUser),
|
||||
readerioresult.Chain(saveUser),
|
||||
)
|
||||
|
||||
// I/O only happens when we call it with a context
|
||||
user, err := pipeline(ctx)
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Build complex operations as pure data structures
|
||||
- Defer execution until needed
|
||||
- Reuse pipelines with different contexts
|
||||
- Test pipelines without executing I/O
|
||||
|
||||
### 5. Context Propagation
|
||||
|
||||
Context is automatically threaded through all operations:
|
||||
|
||||
```go
|
||||
// Traditional - must pass context explicitly everywhere
|
||||
func Process(ctx context.Context) error {
|
||||
user, err := fetchUser(ctx, 42)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
posts, err := fetchPosts(ctx, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return savePosts(ctx, posts)
|
||||
}
|
||||
|
||||
// Functional - context provided once at execution
|
||||
func Process() ReaderIOResult[any] {
|
||||
return F.Pipe2(
|
||||
fetchUser(42),
|
||||
readerioresult.Chain(func(user User) ReaderIOResult[any] {
|
||||
return F.Pipe2(
|
||||
fetchPosts(user.ID),
|
||||
readerioresult.Chain(savePosts),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// Context provided once
|
||||
err := readerioresult.Fold(
|
||||
func(err error) error { return err },
|
||||
func(any) error { return nil },
|
||||
)(Process())(ctx)
|
||||
```
|
||||
|
||||
## Side-by-Side Comparison
|
||||
|
||||
### Example: User Service with Database Operations
|
||||
|
||||
#### Traditional Go Style
|
||||
|
||||
```go
|
||||
package traditional
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
Email string
|
||||
Age int
|
||||
}
|
||||
|
||||
type UserService struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// Fetch user from database
|
||||
func (s *UserService) GetUser(ctx context.Context, id int) (User, error) {
|
||||
var user User
|
||||
|
||||
// Check context
|
||||
if err := ctx.Err(); err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
// Query database
|
||||
row := s.db.QueryRowContext(ctx,
|
||||
"SELECT id, name, email, age FROM users WHERE id = ?", id)
|
||||
|
||||
err := row.Scan(&user.ID, &user.Name, &user.Email, &user.Age)
|
||||
if err != nil {
|
||||
return User{}, fmt.Errorf("scan user: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Validate user
|
||||
func (s *UserService) ValidateUser(ctx context.Context, user User) (User, error) {
|
||||
if user.Age < 18 {
|
||||
return User{}, fmt.Errorf("user %d is too young", user.ID)
|
||||
}
|
||||
if user.Email == "" {
|
||||
return User{}, fmt.Errorf("user %d has no email", user.ID)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Update user email
|
||||
func (s *UserService) UpdateEmail(ctx context.Context, id int, email string) (User, error) {
|
||||
// Check context
|
||||
if err := ctx.Err(); err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
// Update database
|
||||
_, err := s.db.ExecContext(ctx,
|
||||
"UPDATE users SET email = ? WHERE id = ?", email, id)
|
||||
if err != nil {
|
||||
return User{}, fmt.Errorf("update email: %w", err)
|
||||
}
|
||||
|
||||
// Fetch updated user
|
||||
return s.GetUser(ctx, id)
|
||||
}
|
||||
|
||||
// Process user: fetch, validate, update email
|
||||
func (s *UserService) ProcessUser(ctx context.Context, id int, newEmail string) (User, error) {
|
||||
// Fetch user
|
||||
user, err := s.GetUser(ctx, id)
|
||||
if err != nil {
|
||||
return User{}, fmt.Errorf("get user: %w", err)
|
||||
}
|
||||
|
||||
// Validate user
|
||||
validated, err := s.ValidateUser(ctx, user)
|
||||
if err != nil {
|
||||
return User{}, fmt.Errorf("validate user: %w", err)
|
||||
}
|
||||
|
||||
// Update email
|
||||
updated, err := s.UpdateEmail(ctx, validated.ID, newEmail)
|
||||
if err != nil {
|
||||
return User{}, fmt.Errorf("update email: %w", err)
|
||||
}
|
||||
|
||||
return updated, nil
|
||||
}
|
||||
```
|
||||
|
||||
**Characteristics:**
|
||||
- ✗ Manual error checking at every step
|
||||
- ✗ Context passed explicitly to every function
|
||||
- ✗ Error wrapping is manual and verbose
|
||||
- ✗ Business logic mixed with error handling
|
||||
- ✗ Hard to test without database
|
||||
- ✗ Difficult to compose operations
|
||||
- ✓ Familiar to Go developers
|
||||
- ✓ Explicit control flow
|
||||
|
||||
#### Functional Go Style (context/readerioresult)
|
||||
|
||||
```go
|
||||
package functional
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
Email string
|
||||
Age int
|
||||
}
|
||||
|
||||
type UserService struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// Fetch user from database - returns a ReaderIOResult
|
||||
func (s *UserService) GetUser(id int) RIO.ReaderIOResult[User] {
|
||||
return func(ctx context.Context) (User, error) {
|
||||
var user User
|
||||
row := s.db.QueryRowContext(ctx,
|
||||
"SELECT id, name, email, age FROM users WHERE id = ?", id)
|
||||
|
||||
err := row.Scan(&user.ID, &user.Name, &user.Email, &user.Age)
|
||||
if err != nil {
|
||||
return User{}, fmt.Errorf("scan user: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Validate user - pure function (no I/O, no context)
|
||||
func ValidateUser(user User) (User, error) {
|
||||
if user.Age < 18 {
|
||||
return User{}, fmt.Errorf("user %d is too young", user.ID)
|
||||
}
|
||||
if user.Email == "" {
|
||||
return User{}, fmt.Errorf("user %d has no email", user.ID)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Update user email - returns a ReaderIOResult
|
||||
func (s *UserService) UpdateEmail(id int, email string) RIO.ReaderIOResult[User] {
|
||||
return func(ctx context.Context) (User, error) {
|
||||
_, err := s.db.ExecContext(ctx,
|
||||
"UPDATE users SET email = ? WHERE id = ?", email, id)
|
||||
if err != nil {
|
||||
return User{}, fmt.Errorf("update email: %w", err)
|
||||
}
|
||||
|
||||
// Chain to GetUser
|
||||
return s.GetUser(id)(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Process user: fetch, validate, update email - composable pipeline
|
||||
func (s *UserService) ProcessUser(id int, newEmail string) RIO.ReaderIOResult[User] {
|
||||
return F.Pipe3(
|
||||
s.GetUser(id), // Fetch user
|
||||
RIO.ChainEitherK(ValidateUser), // Validate (pure function)
|
||||
RIO.Chain(func(user User) RIO.ReaderIOResult[User] {
|
||||
return s.UpdateEmail(user.ID, newEmail) // Update email
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// Alternative: Using Do-notation for more complex flows
|
||||
func (s *UserService) ProcessUserDo(id int, newEmail string) RIO.ReaderIOResult[User] {
|
||||
return RIO.Chain(func(user User) RIO.ReaderIOResult[User] {
|
||||
// Validate is pure, lift it into ReaderIOResult
|
||||
validated, err := ValidateUser(user)
|
||||
if err != nil {
|
||||
return RIO.Left[User](err)
|
||||
}
|
||||
// Update with validated user
|
||||
return s.UpdateEmail(validated.ID, newEmail)
|
||||
})(s.GetUser(id))
|
||||
}
|
||||
```
|
||||
|
||||
**Characteristics:**
|
||||
- ✓ Automatic error propagation (no manual checking)
|
||||
- ✓ Context threaded automatically
|
||||
- ✓ Pure functions separated from I/O
|
||||
- ✓ Business logic clear and composable
|
||||
- ✓ Easy to test (mock ReaderIOResult)
|
||||
- ✓ Operations compose naturally
|
||||
- ✓ Lazy evaluation (build pipeline, execute later)
|
||||
- ✗ Requires understanding of functional patterns
|
||||
- ✗ Less familiar to traditional Go developers
|
||||
|
||||
#### Idiomatic Functional Style (idiomatic/context/readerresult)
|
||||
|
||||
For even better performance with the same functional benefits:
|
||||
|
||||
```go
|
||||
package idiomatic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
RR "github.com/IBM/fp-go/v2/idiomatic/context/readerresult"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
Email string
|
||||
Age int
|
||||
}
|
||||
|
||||
type UserService struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// ReaderResult is just: func(context.Context) (A, error)
|
||||
// Same as ReaderIOResult but using native Go tuples
|
||||
|
||||
func (s *UserService) GetUser(id int) RR.ReaderResult[User] {
|
||||
return func(ctx context.Context) (User, error) {
|
||||
var user User
|
||||
row := s.db.QueryRowContext(ctx,
|
||||
"SELECT id, name, email, age FROM users WHERE id = ?", id)
|
||||
|
||||
err := row.Scan(&user.ID, &user.Name, &user.Email, &user.Age)
|
||||
return user, err // Native tuple return
|
||||
}
|
||||
}
|
||||
|
||||
// Pure validation - returns native (User, error) tuple
|
||||
func ValidateUser(user User) (User, error) {
|
||||
if user.Age < 18 {
|
||||
return User{}, fmt.Errorf("user %d is too young", user.ID)
|
||||
}
|
||||
if user.Email == "" {
|
||||
return User{}, fmt.Errorf("user %d has no email", user.ID)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *UserService) UpdateEmail(id int, email string) RR.ReaderResult[User] {
|
||||
return func(ctx context.Context) (User, error) {
|
||||
_, err := s.db.ExecContext(ctx,
|
||||
"UPDATE users SET email = ? WHERE id = ?", email, id)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
return s.GetUser(id)(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Composable pipeline with native tuples
|
||||
func (s *UserService) ProcessUser(id int, newEmail string) RR.ReaderResult[User] {
|
||||
return F.Pipe3(
|
||||
s.GetUser(id),
|
||||
RR.ChainEitherK(ValidateUser), // Lift pure function
|
||||
RR.Chain(func(user User) RR.ReaderResult[User] {
|
||||
return s.UpdateEmail(user.ID, newEmail)
|
||||
}),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Characteristics:**
|
||||
- ✓ All benefits of functional approach
|
||||
- ✓ **2-10x better performance** (native tuples)
|
||||
- ✓ **Zero allocations** for many operations
|
||||
- ✓ More familiar to Go developers (uses (value, error))
|
||||
- ✓ Seamless integration with existing Go code
|
||||
- ✓ Same composability as ReaderIOResult
|
||||
|
||||
### Usage Comparison
|
||||
|
||||
```go
|
||||
// Traditional
|
||||
func HandleRequest(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
service := &UserService{db: db}
|
||||
|
||||
user, err := service.ProcessUser(ctx, 42, "new@email.com")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(user)
|
||||
}
|
||||
|
||||
// Functional (both styles)
|
||||
func HandleRequest(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
service := &UserService{db: db}
|
||||
|
||||
// Build the pipeline (no execution yet)
|
||||
pipeline := service.ProcessUser(42, "new@email.com")
|
||||
|
||||
// Execute with context
|
||||
user, err := pipeline(ctx)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(user)
|
||||
}
|
||||
|
||||
// Or using Fold for cleaner error handling
|
||||
func HandleRequestFold(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
service := &UserService{db: db}
|
||||
|
||||
RR.Fold(
|
||||
func(err error) {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
},
|
||||
func(user User) {
|
||||
json.NewEncoder(w).Encode(user)
|
||||
},
|
||||
)(service.ProcessUser(42, "new@email.com"))(ctx)
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Resource Management with Bracket
|
||||
|
||||
```go
|
||||
// Traditional
|
||||
func ProcessFile(ctx context.Context, path string) (string, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// Functional - guaranteed cleanup even on panic
|
||||
func ProcessFile(path string) RIO.ReaderIOResult[string] {
|
||||
return RIO.Bracket(
|
||||
// Acquire resource
|
||||
func(ctx context.Context) (*os.File, error) {
|
||||
return os.Open(path)
|
||||
},
|
||||
// Release resource (always called)
|
||||
func(file *os.File, err error) RIO.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) (any, error) {
|
||||
return nil, file.Close()
|
||||
}
|
||||
},
|
||||
// Use resource
|
||||
func(file *os.File) RIO.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) (string, error) {
|
||||
data, err := io.ReadAll(file)
|
||||
return string(data), err
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Parallel Execution
|
||||
|
||||
```go
|
||||
// Traditional - manual goroutines and sync
|
||||
func FetchMultipleUsers(ctx context.Context, ids []int) ([]User, error) {
|
||||
var wg sync.WaitGroup
|
||||
users := make([]User, len(ids))
|
||||
errs := make([]error, len(ids))
|
||||
|
||||
for i, id := range ids {
|
||||
wg.Add(1)
|
||||
go func(i, id int) {
|
||||
defer wg.Done()
|
||||
users[i], errs[i] = fetchUser(ctx, id)
|
||||
}(i, id)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// Functional - automatic parallelization
|
||||
func FetchMultipleUsers(ids []int) RIO.ReaderIOResult[[]User] {
|
||||
operations := A.Map(func(id int) RIO.ReaderIOResult[User] {
|
||||
return fetchUser(id)
|
||||
})(ids)
|
||||
|
||||
return RIO.TraverseArrayPar(F.Identity[RIO.ReaderIOResult[User]])(operations)
|
||||
}
|
||||
```
|
||||
|
||||
### Retry Logic
|
||||
|
||||
```go
|
||||
// Traditional
|
||||
func FetchWithRetry(ctx context.Context, url string, maxRetries int) ([]byte, error) {
|
||||
var lastErr error
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
time.Sleep(time.Second * time.Duration(i+1))
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
// Functional
|
||||
func FetchWithRetry(url string, maxRetries int) RIO.ReaderIOResult[[]byte] {
|
||||
operation := func(ctx context.Context) ([]byte, error) {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
return RIO.Retry(
|
||||
maxRetries,
|
||||
func(attempt int) time.Duration {
|
||||
return time.Second * time.Duration(attempt)
|
||||
},
|
||||
)(operation)
|
||||
}
|
||||
```
|
||||
|
||||
## When to Use Each Approach
|
||||
|
||||
### Use Traditional Go Style When:
|
||||
|
||||
1. **Team familiarity**: Team is not familiar with functional programming
|
||||
2. **Simple operations**: Single I/O operation with straightforward error handling
|
||||
3. **Existing codebase**: Large codebase already using traditional patterns
|
||||
4. **Learning curve**: Want to minimize onboarding time
|
||||
5. **Explicit control**: Need very explicit control flow
|
||||
|
||||
### Use Functional Style (ReaderIOResult) When:
|
||||
|
||||
1. **Complex pipelines**: Multiple I/O operations that need composition
|
||||
2. **Testability**: Need to test business logic separately from I/O
|
||||
3. **Reusability**: Want to build reusable operation pipelines
|
||||
4. **Error handling**: Want automatic error propagation
|
||||
5. **Resource management**: Need guaranteed cleanup (Bracket)
|
||||
6. **Parallel execution**: Need to parallelize operations easily
|
||||
7. **Type safety**: Want the type system to track I/O effects
|
||||
|
||||
### Use Idiomatic Functional Style (idiomatic/context/readerresult) When:
|
||||
|
||||
1. **All functional benefits**: Want functional patterns with Go idioms
|
||||
2. **Performance critical**: Need 2-10x better performance
|
||||
3. **Zero allocations**: Memory efficiency is important
|
||||
4. **Go integration**: Want seamless integration with existing Go code
|
||||
5. **Production services**: Building high-throughput services
|
||||
6. **Best of both worlds**: Want functional composition with Go's native patterns
|
||||
|
||||
## Summary
|
||||
|
||||
The functional approach to I/O in Go offers significant advantages:
|
||||
|
||||
1. **Separation of Concerns**: Pure logic separated from I/O effects
|
||||
2. **Composability**: Operations compose naturally without manual error checking
|
||||
3. **Testability**: Easy to test without mocking I/O
|
||||
4. **Type Safety**: I/O effects visible in the type system
|
||||
5. **Lazy Evaluation**: Build pipelines, execute when ready
|
||||
6. **Context Propagation**: Automatic threading of context
|
||||
7. **Performance**: Idiomatic version offers 2-10x speedup
|
||||
|
||||
The key insight is that **I/O operations return descriptions of effects** (ReaderIOResult) rather than performing effects immediately. This enables powerful composition patterns while maintaining Go's idiomatic error handling through the `(value, error)` tuple pattern.
|
||||
|
||||
For production Go services, the **idiomatic/context/readerresult** package provides the best balance: full functional programming capabilities with native Go performance and familiar error handling patterns.
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [DESIGN.md](./DESIGN.md) - Design principles and patterns
|
||||
- [IDIOMATIC_COMPARISON.md](./IDIOMATIC_COMPARISON.md) - Performance comparison
|
||||
- [idiomatic/doc.go](./idiomatic/doc.go) - Idiomatic package overview
|
||||
- [context/readerioresult](./context/readerioresult/) - ReaderIOResult package
|
||||
- [idiomatic/context/readerresult](./idiomatic/context/readerresult/) - Idiomatic ReaderResult package
|
||||
@@ -447,6 +447,8 @@ func process() IOResult[string] {
|
||||
## 📚 Documentation
|
||||
|
||||
- **[Design Decisions](./DESIGN.md)** - Key design principles and patterns explained
|
||||
- **[Functional I/O in Go](./FUNCTIONAL_IO.md)** - Understanding Context, errors, and the Reader pattern for I/O operations
|
||||
- **[Idiomatic vs Standard Packages](./IDIOMATIC_COMPARISON.md)** - Performance comparison and when to use each approach
|
||||
- **[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
|
||||
@@ -463,7 +465,7 @@ func process() IOResult[string] {
|
||||
- **ReaderIOResult** - Combine Reader, IO, and Result for complex workflows
|
||||
- **Array** - Functional array operations
|
||||
- **Record** - Functional record/map operations
|
||||
- **Optics** - Lens, Prism, Optional, and Traversal for immutable updates
|
||||
- **[Optics](./optics/README.md)** - Lens, Prism, Optional, and Traversal for immutable updates
|
||||
|
||||
#### Idiomatic Packages (Tuple-based, High Performance)
|
||||
- **idiomatic/option** - Option monad using native Go `(value, bool)` tuples
|
||||
|
||||
@@ -190,6 +190,11 @@ func MonadReduce[A, B any](fa []A, f func(B, A) B, initial B) B {
|
||||
return G.MonadReduce(fa, f, initial)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadReduceWithIndex[A, B any](fa []A, f func(int, B, A) B, initial B) B {
|
||||
return G.MonadReduceWithIndex(fa, f, initial)
|
||||
}
|
||||
|
||||
// Reduce folds an array from left to right, applying a function to accumulate a result.
|
||||
//
|
||||
// Example:
|
||||
|
||||
@@ -764,14 +764,14 @@ func TestFoldMap(t *testing.T) {
|
||||
t.Run("FoldMap with sum semigroup", func(t *testing.T) {
|
||||
sumSemigroup := N.SemigroupSum[int]()
|
||||
arr := From(1, 2, 3, 4)
|
||||
result := FoldMap[int, int](sumSemigroup)(func(x int) int { return x * 2 })(arr)
|
||||
result := FoldMap[int](sumSemigroup)(func(x int) int { return x * 2 })(arr)
|
||||
assert.Equal(t, 20, result) // (1*2) + (2*2) + (3*2) + (4*2) = 20
|
||||
})
|
||||
|
||||
t.Run("FoldMap with string concatenation", func(t *testing.T) {
|
||||
concatSemigroup := STR.Semigroup
|
||||
arr := From(1, 2, 3)
|
||||
result := FoldMap[int, string](concatSemigroup)(func(x int) string { return fmt.Sprintf("%d", x) })(arr)
|
||||
result := FoldMap[int](concatSemigroup)(func(x int) string { return fmt.Sprintf("%d", x) })(arr)
|
||||
assert.Equal(t, "123", result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func generateTraverseTuple(f *os.File, i int) {
|
||||
@@ -422,10 +423,10 @@ func ApplyCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateApplyHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func createCombinations(n int, all, prev []int) [][]int {
|
||||
@@ -284,10 +285,10 @@ func BindCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateBindHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func Commands() []*C.Command {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -23,7 +24,7 @@ import (
|
||||
"strings"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// Deprecated:
|
||||
@@ -261,10 +262,10 @@ func ContextReaderIOEitherCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateContextReaderIOEitherHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func generateMakeProvider(f *os.File, i int) {
|
||||
@@ -221,10 +222,10 @@ func DICommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateDIHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func eitherHKT(typeE string) func(typeA string) string {
|
||||
@@ -190,10 +191,10 @@ func EitherCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateEitherHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func identityHKT(typeA string) string {
|
||||
@@ -93,10 +94,10 @@ func IdentityCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateIdentityHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -23,7 +24,7 @@ import (
|
||||
"time"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func nonGenericIO(param string) string {
|
||||
@@ -102,10 +103,10 @@ func IOCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateIOHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -23,7 +24,7 @@ import (
|
||||
"time"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// [GA ~func() ET.Either[E, A], GB ~func() ET.Either[E, B], GTAB ~func() ET.Either[E, T.Tuple2[A, B]], E, A, B any](a GA, b GB) GTAB {
|
||||
@@ -273,10 +274,10 @@ func IOEitherCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateIOEitherHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -23,7 +24,7 @@ import (
|
||||
"time"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func nonGenericIOOption(param string) string {
|
||||
@@ -107,10 +108,10 @@ func IOOptionCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateIOOptionHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
@@ -28,7 +29,7 @@ import (
|
||||
"text/template"
|
||||
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -535,9 +536,9 @@ func extractEmbeddedFields(embedType ast.Expr, fileImports map[string]string, fi
|
||||
}
|
||||
|
||||
for _, name := range field.Names {
|
||||
// Only export lenses for exported fields
|
||||
if name.IsExported() {
|
||||
fieldTypeName := getTypeName(field.Type)
|
||||
// Generate lenses for both exported and unexported fields
|
||||
fieldTypeName := getTypeName(field.Type)
|
||||
if true { // Keep the block structure for minimal changes
|
||||
isOptional := false
|
||||
baseType := fieldTypeName
|
||||
|
||||
@@ -697,9 +698,9 @@ func parseFile(filename string) ([]structInfo, string, error) {
|
||||
continue
|
||||
}
|
||||
for _, name := range field.Names {
|
||||
// Only export lenses for exported fields
|
||||
if name.IsExported() {
|
||||
typeName := getTypeName(field.Type)
|
||||
// Generate lenses for both exported and unexported fields
|
||||
typeName := getTypeName(field.Type)
|
||||
if true { // Keep the block structure for minimal changes
|
||||
isOptional := false
|
||||
baseType := typeName
|
||||
isComparable := false
|
||||
@@ -934,12 +935,12 @@ func LensCommand() *C.Command {
|
||||
flagVerbose,
|
||||
flagIncludeTestFiles,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateLensHelpers(
|
||||
ctx.String(keyLensDir),
|
||||
ctx.String(keyFilename),
|
||||
ctx.Bool(keyVerbose),
|
||||
ctx.Bool(keyIncludeTestFile),
|
||||
cmd.String(keyLensDir),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Bool(keyVerbose),
|
||||
cmd.Bool(keyIncludeTestFile),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1086,3 +1086,255 @@ type ComparableBox[T comparable] struct {
|
||||
// Verify that MakeLensRef is NOT used (since both fields are comparable)
|
||||
assert.NotContains(t, contentStr, "__lens.MakeLensRefWithName(", "Should not use MakeLensRefWithName when all fields are comparable")
|
||||
}
|
||||
|
||||
func TestParseFileWithUnexportedFields(t *testing.T) {
|
||||
// Create a temporary test file
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
|
||||
testCode := `package testpkg
|
||||
|
||||
// fp-go:Lens
|
||||
type Config struct {
|
||||
PublicName string
|
||||
privateName string
|
||||
PublicValue int
|
||||
privateValue *int
|
||||
}
|
||||
`
|
||||
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Parse the file
|
||||
structs, pkg, err := parseFile(testFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify results
|
||||
assert.Equal(t, "testpkg", pkg)
|
||||
assert.Len(t, structs, 1)
|
||||
|
||||
// Check Config struct
|
||||
config := structs[0]
|
||||
assert.Equal(t, "Config", config.Name)
|
||||
assert.Len(t, config.Fields, 4, "Should include both exported and unexported fields")
|
||||
|
||||
// Check exported field
|
||||
assert.Equal(t, "PublicName", config.Fields[0].Name)
|
||||
assert.Equal(t, "string", config.Fields[0].TypeName)
|
||||
assert.False(t, config.Fields[0].IsOptional)
|
||||
|
||||
// Check unexported field
|
||||
assert.Equal(t, "privateName", config.Fields[1].Name)
|
||||
assert.Equal(t, "string", config.Fields[1].TypeName)
|
||||
assert.False(t, config.Fields[1].IsOptional)
|
||||
|
||||
// Check exported int field
|
||||
assert.Equal(t, "PublicValue", config.Fields[2].Name)
|
||||
assert.Equal(t, "int", config.Fields[2].TypeName)
|
||||
assert.False(t, config.Fields[2].IsOptional)
|
||||
|
||||
// Check unexported pointer field
|
||||
assert.Equal(t, "privateValue", config.Fields[3].Name)
|
||||
assert.Equal(t, "*int", config.Fields[3].TypeName)
|
||||
assert.True(t, config.Fields[3].IsOptional)
|
||||
}
|
||||
|
||||
func TestGenerateLensHelpersWithUnexportedFields(t *testing.T) {
|
||||
// Create a temporary directory with test files
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
testCode := `package testpkg
|
||||
|
||||
// fp-go:Lens
|
||||
type MixedStruct struct {
|
||||
PublicField string
|
||||
privateField int
|
||||
OptionalPrivate *string
|
||||
}
|
||||
`
|
||||
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate lens code
|
||||
outputFile := "gen_lens.go"
|
||||
err = generateLensHelpers(tmpDir, outputFile, false, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the generated file exists
|
||||
genPath := filepath.Join(tmpDir, outputFile)
|
||||
_, err = os.Stat(genPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Read and verify the generated content
|
||||
content, err := os.ReadFile(genPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
contentStr := string(content)
|
||||
|
||||
// Check for expected content
|
||||
assert.Contains(t, contentStr, "package testpkg")
|
||||
assert.Contains(t, contentStr, "MixedStructLenses")
|
||||
assert.Contains(t, contentStr, "MakeMixedStructLenses")
|
||||
|
||||
// Check that lenses are generated for all fields (exported and unexported)
|
||||
assert.Contains(t, contentStr, "PublicField __lens.Lens[MixedStruct, string]")
|
||||
assert.Contains(t, contentStr, "privateField __lens.Lens[MixedStruct, int]")
|
||||
assert.Contains(t, contentStr, "OptionalPrivate __lens.Lens[MixedStruct, *string]")
|
||||
|
||||
// Check lens constructors
|
||||
assert.Contains(t, contentStr, "func(s MixedStruct) string { return s.PublicField }")
|
||||
assert.Contains(t, contentStr, "func(s MixedStruct) int { return s.privateField }")
|
||||
assert.Contains(t, contentStr, "func(s MixedStruct) *string { return s.OptionalPrivate }")
|
||||
|
||||
// Check setters
|
||||
assert.Contains(t, contentStr, "func(s MixedStruct, v string) MixedStruct { s.PublicField = v; return s }")
|
||||
assert.Contains(t, contentStr, "func(s MixedStruct, v int) MixedStruct { s.privateField = v; return s }")
|
||||
assert.Contains(t, contentStr, "func(s MixedStruct, v *string) MixedStruct { s.OptionalPrivate = v; return s }")
|
||||
}
|
||||
|
||||
func TestParseFileWithOnlyUnexportedFields(t *testing.T) {
|
||||
// Create a temporary test file
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
|
||||
testCode := `package testpkg
|
||||
|
||||
// fp-go:Lens
|
||||
type PrivateConfig struct {
|
||||
name string
|
||||
value int
|
||||
enabled bool
|
||||
}
|
||||
`
|
||||
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Parse the file
|
||||
structs, pkg, err := parseFile(testFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify results
|
||||
assert.Equal(t, "testpkg", pkg)
|
||||
assert.Len(t, structs, 1)
|
||||
|
||||
// Check PrivateConfig struct
|
||||
config := structs[0]
|
||||
assert.Equal(t, "PrivateConfig", config.Name)
|
||||
assert.Len(t, config.Fields, 3, "Should include all unexported fields")
|
||||
|
||||
// Check all fields are unexported
|
||||
assert.Equal(t, "name", config.Fields[0].Name)
|
||||
assert.Equal(t, "value", config.Fields[1].Name)
|
||||
assert.Equal(t, "enabled", config.Fields[2].Name)
|
||||
}
|
||||
|
||||
func TestGenerateLensHelpersWithUnexportedEmbeddedFields(t *testing.T) {
|
||||
// Create a temporary directory with test files
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
testCode := `package testpkg
|
||||
|
||||
type BaseConfig struct {
|
||||
publicBase string
|
||||
privateBase int
|
||||
}
|
||||
|
||||
// fp-go:Lens
|
||||
type ExtendedConfig struct {
|
||||
BaseConfig
|
||||
PublicField string
|
||||
privateField bool
|
||||
}
|
||||
`
|
||||
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate lens code
|
||||
outputFile := "gen_lens.go"
|
||||
err = generateLensHelpers(tmpDir, outputFile, false, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the generated file exists
|
||||
genPath := filepath.Join(tmpDir, outputFile)
|
||||
_, err = os.Stat(genPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Read and verify the generated content
|
||||
content, err := os.ReadFile(genPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
contentStr := string(content)
|
||||
|
||||
// Check for expected content
|
||||
assert.Contains(t, contentStr, "package testpkg")
|
||||
assert.Contains(t, contentStr, "ExtendedConfigLenses")
|
||||
|
||||
// Check that lenses are generated for embedded unexported fields
|
||||
assert.Contains(t, contentStr, "publicBase __lens.Lens[ExtendedConfig, string]")
|
||||
assert.Contains(t, contentStr, "privateBase __lens.Lens[ExtendedConfig, int]")
|
||||
|
||||
// Check that lenses are generated for direct fields (both exported and unexported)
|
||||
assert.Contains(t, contentStr, "PublicField __lens.Lens[ExtendedConfig, string]")
|
||||
assert.Contains(t, contentStr, "privateField __lens.Lens[ExtendedConfig, bool]")
|
||||
}
|
||||
|
||||
func TestParseFileWithMixedFieldVisibility(t *testing.T) {
|
||||
// Create a temporary test file with various field visibility patterns
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
|
||||
testCode := `package testpkg
|
||||
|
||||
// fp-go:Lens
|
||||
type ComplexStruct struct {
|
||||
// Exported fields
|
||||
Name string
|
||||
Age int
|
||||
Email *string
|
||||
|
||||
// Unexported fields
|
||||
password string
|
||||
secretKey []byte
|
||||
internalID *int
|
||||
|
||||
// Mixed with tags
|
||||
PublicWithTag string ` + "`json:\"public,omitempty\"`" + `
|
||||
privateWithTag int ` + "`json:\"private,omitempty\"`" + `
|
||||
}
|
||||
`
|
||||
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Parse the file
|
||||
structs, pkg, err := parseFile(testFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify results
|
||||
assert.Equal(t, "testpkg", pkg)
|
||||
assert.Len(t, structs, 1)
|
||||
|
||||
// Check ComplexStruct
|
||||
complex := structs[0]
|
||||
assert.Equal(t, "ComplexStruct", complex.Name)
|
||||
assert.Len(t, complex.Fields, 8, "Should include all fields regardless of visibility")
|
||||
|
||||
// Verify field names and types
|
||||
fieldNames := []string{"Name", "Age", "Email", "password", "secretKey", "internalID", "PublicWithTag", "privateWithTag"}
|
||||
for i, expectedName := range fieldNames {
|
||||
assert.Equal(t, expectedName, complex.Fields[i].Name, "Field %d should be %s", i, expectedName)
|
||||
}
|
||||
|
||||
// Check optional fields
|
||||
assert.False(t, complex.Fields[0].IsOptional, "Name should not be optional")
|
||||
assert.True(t, complex.Fields[2].IsOptional, "Email (pointer) should be optional")
|
||||
assert.True(t, complex.Fields[5].IsOptional, "internalID (pointer) should be optional")
|
||||
assert.True(t, complex.Fields[6].IsOptional, "PublicWithTag (with omitempty) should be optional")
|
||||
assert.True(t, complex.Fields[7].IsOptional, "privateWithTag (with omitempty) should be optional")
|
||||
}
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func optionHKT(typeA string) string {
|
||||
@@ -200,10 +201,10 @@ func OptionCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateOptionHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func generateUnsliced(f *os.File, i int) {
|
||||
@@ -423,10 +424,10 @@ func PipeCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generatePipeHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func generateReaderFrom(f, fg *os.File, i int) {
|
||||
@@ -154,10 +155,10 @@ func ReaderCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateReaderHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func generateReaderIOEitherFrom(f, fg *os.File, i int) {
|
||||
@@ -284,10 +285,10 @@ func ReaderIOEitherCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateReaderIOEitherHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -23,7 +24,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func writeTupleType(f *os.File, symbol string, i int) {
|
||||
@@ -615,10 +616,10 @@ func TupleCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateTupleHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -177,3 +177,255 @@ func Local[R1, R2 any](f func(R2) R1) Operator[R1, R2] {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose is an alias for Local that emphasizes the composition aspect of consumer transformation.
|
||||
// It composes a preprocessing function with a consumer, creating a new consumer that applies
|
||||
// the function before consuming the value.
|
||||
//
|
||||
// This function is semantically identical to Local but uses terminology that may be more familiar
|
||||
// to developers coming from functional programming backgrounds where "compose" is a common operation.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// The name "Compose" highlights that we're composing two operations:
|
||||
// 1. The transformation function f: R2 -> R1
|
||||
// 2. The consumer c: R1 -> ()
|
||||
//
|
||||
// Result: A composed consumer: R2 -> ()
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R1: The input type of the original Consumer (what it expects)
|
||||
// - R2: The input type of the new Consumer (what you have)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that converts R2 to R1 (preprocessing function)
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms Consumer[R1] into Consumer[R2]
|
||||
//
|
||||
// Example - Basic composition:
|
||||
//
|
||||
// // Consumer that logs integers
|
||||
// logInt := func(x int) {
|
||||
// fmt.Printf("Value: %d\n", x)
|
||||
// }
|
||||
//
|
||||
// // Compose with a string-to-int parser
|
||||
// parseToInt := func(s string) int {
|
||||
// n, _ := strconv.Atoi(s)
|
||||
// return n
|
||||
// }
|
||||
//
|
||||
// logString := consumer.Compose(parseToInt)(logInt)
|
||||
// logString("42") // Logs: "Value: 42"
|
||||
//
|
||||
// Example - Composing multiple transformations:
|
||||
//
|
||||
// type Data struct {
|
||||
// Value string
|
||||
// }
|
||||
//
|
||||
// type Wrapper struct {
|
||||
// Data Data
|
||||
// }
|
||||
//
|
||||
// // Consumer that logs strings
|
||||
// logString := func(s string) {
|
||||
// fmt.Println(s)
|
||||
// }
|
||||
//
|
||||
// // Compose transformations step by step
|
||||
// extractData := func(w Wrapper) Data { return w.Data }
|
||||
// extractValue := func(d Data) string { return d.Value }
|
||||
//
|
||||
// logData := consumer.Compose(extractValue)(logString)
|
||||
// logWrapper := consumer.Compose(extractData)(logData)
|
||||
//
|
||||
// logWrapper(Wrapper{Data: Data{Value: "Hello"}}) // Logs: "Hello"
|
||||
//
|
||||
// Example - Function composition style:
|
||||
//
|
||||
// // Compose is particularly useful when thinking in terms of function composition
|
||||
// type Request struct {
|
||||
// Body []byte
|
||||
// }
|
||||
//
|
||||
// // Consumer that processes strings
|
||||
// processString := func(s string) {
|
||||
// fmt.Printf("Processing: %s\n", s)
|
||||
// }
|
||||
//
|
||||
// // Compose byte-to-string conversion with processing
|
||||
// bytesToString := func(b []byte) string {
|
||||
// return string(b)
|
||||
// }
|
||||
// extractBody := func(r Request) []byte {
|
||||
// return r.Body
|
||||
// }
|
||||
//
|
||||
// // Chain compositions
|
||||
// processBytes := consumer.Compose(bytesToString)(processString)
|
||||
// processRequest := consumer.Compose(extractBody)(processBytes)
|
||||
//
|
||||
// processRequest(Request{Body: []byte("test")}) // Logs: "Processing: test"
|
||||
//
|
||||
// Relationship to Local:
|
||||
// - Compose and Local are identical in implementation
|
||||
// - Compose emphasizes the functional composition aspect
|
||||
// - Local emphasizes the environment/context transformation aspect
|
||||
// - Use Compose when thinking about function composition
|
||||
// - Use Local when thinking about adapting to different contexts
|
||||
//
|
||||
// Use Cases:
|
||||
// - Building processing pipelines with clear composition semantics
|
||||
// - Adapting consumers in a functional programming style
|
||||
// - Creating reusable consumer transformations
|
||||
// - Chaining multiple preprocessing steps
|
||||
func Compose[R1, R2 any](f func(R2) R1) Operator[R1, R2] {
|
||||
return Local(f)
|
||||
}
|
||||
|
||||
// Contramap is the categorical name for the contravariant functor operation on Consumers.
|
||||
// It transforms a Consumer by preprocessing its input, making it the dual of the covariant
|
||||
// functor's map operation.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#contravariant
|
||||
//
|
||||
// In category theory, a contravariant functor reverses the direction of morphisms.
|
||||
// While a covariant functor maps f: A -> B to map(f): F[A] -> F[B],
|
||||
// a contravariant functor maps f: A -> B to contramap(f): F[B] -> F[A].
|
||||
//
|
||||
// For Consumers:
|
||||
// - Consumer[A] is contravariant in A
|
||||
// - Given f: R2 -> R1, contramap(f) transforms Consumer[R1] to Consumer[R2]
|
||||
// - The direction is reversed: we go from Consumer[R1] to Consumer[R2]
|
||||
//
|
||||
// This is semantically identical to Local and Compose, but uses the standard
|
||||
// categorical terminology that emphasizes the contravariant nature of the transformation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R1: The input type of the original Consumer (what it expects)
|
||||
// - R2: The input type of the new Consumer (what you have)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that converts R2 to R1 (preprocessing function)
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms Consumer[R1] into Consumer[R2]
|
||||
//
|
||||
// Example - Basic contravariant mapping:
|
||||
//
|
||||
// // Consumer that logs integers
|
||||
// logInt := func(x int) {
|
||||
// fmt.Printf("Value: %d\n", x)
|
||||
// }
|
||||
//
|
||||
// // Contramap with a string-to-int parser
|
||||
// parseToInt := func(s string) int {
|
||||
// n, _ := strconv.Atoi(s)
|
||||
// return n
|
||||
// }
|
||||
//
|
||||
// logString := consumer.Contramap(parseToInt)(logInt)
|
||||
// logString("42") // Logs: "Value: 42"
|
||||
//
|
||||
// Example - Demonstrating contravariance:
|
||||
//
|
||||
// // In covariant functors (like Option, Array), map goes "forward":
|
||||
// // map: (A -> B) -> F[A] -> F[B]
|
||||
// //
|
||||
// // In contravariant functors (like Consumer), contramap goes "backward":
|
||||
// // contramap: (B -> A) -> F[A] -> F[B]
|
||||
//
|
||||
// type Animal struct{ Name string }
|
||||
// type Dog struct{ Animal Animal; Breed string }
|
||||
//
|
||||
// // Consumer for animals
|
||||
// consumeAnimal := func(a Animal) {
|
||||
// fmt.Printf("Animal: %s\n", a.Name)
|
||||
// }
|
||||
//
|
||||
// // Function from Dog to Animal (B -> A)
|
||||
// dogToAnimal := func(d Dog) Animal {
|
||||
// return d.Animal
|
||||
// }
|
||||
//
|
||||
// // Contramap creates Consumer[Dog] from Consumer[Animal]
|
||||
// // Direction is reversed: Consumer[Animal] -> Consumer[Dog]
|
||||
// consumeDog := consumer.Contramap(dogToAnimal)(consumeAnimal)
|
||||
//
|
||||
// consumeDog(Dog{
|
||||
// Animal: Animal{Name: "Buddy"},
|
||||
// Breed: "Golden Retriever",
|
||||
// }) // Logs: "Animal: Buddy"
|
||||
//
|
||||
// Example - Contravariant functor laws:
|
||||
//
|
||||
// // Law 1: Identity
|
||||
// // contramap(identity) = identity
|
||||
// identity := func(x int) int { return x }
|
||||
// consumer1 := consumer.Contramap(identity)(consumeInt)
|
||||
// // consumer1 behaves identically to consumeInt
|
||||
//
|
||||
// // Law 2: Composition
|
||||
// // contramap(f . g) = contramap(g) . contramap(f)
|
||||
// // Note: composition order is reversed compared to covariant map
|
||||
// f := func(s string) int { n, _ := strconv.Atoi(s); return n }
|
||||
// g := func(b bool) string { if b { return "1" } else { return "0" } }
|
||||
//
|
||||
// // These two are equivalent:
|
||||
// consumer2 := consumer.Contramap(func(b bool) int { return f(g(b)) })(consumeInt)
|
||||
// consumer3 := consumer.Contramap(g)(consumer.Contramap(f)(consumeInt))
|
||||
//
|
||||
// Example - Practical use with type hierarchies:
|
||||
//
|
||||
// type Logger interface {
|
||||
// Log(string)
|
||||
// }
|
||||
//
|
||||
// type Message struct {
|
||||
// Text string
|
||||
// Timestamp time.Time
|
||||
// }
|
||||
//
|
||||
// // Consumer that logs strings
|
||||
// logString := func(s string) {
|
||||
// fmt.Println(s)
|
||||
// }
|
||||
//
|
||||
// // Contramap to handle Message types
|
||||
// extractText := func(m Message) string {
|
||||
// return fmt.Sprintf("[%s] %s", m.Timestamp.Format(time.RFC3339), m.Text)
|
||||
// }
|
||||
//
|
||||
// logMessage := consumer.Contramap(extractText)(logString)
|
||||
// logMessage(Message{
|
||||
// Text: "Hello",
|
||||
// Timestamp: time.Now(),
|
||||
// }) // Logs: "[2024-01-20T10:00:00Z] Hello"
|
||||
//
|
||||
// Relationship to Local and Compose:
|
||||
// - Contramap, Local, and Compose are identical in implementation
|
||||
// - Contramap emphasizes the categorical/theoretical aspect
|
||||
// - Local emphasizes the context transformation aspect
|
||||
// - Compose emphasizes the function composition aspect
|
||||
// - Use Contramap when working with category theory concepts
|
||||
// - Use Local when adapting to different contexts
|
||||
// - Use Compose when building functional pipelines
|
||||
//
|
||||
// Category Theory Background:
|
||||
// - Consumer[A] forms a contravariant functor
|
||||
// - The contravariant functor laws must hold:
|
||||
// 1. contramap(id) = id
|
||||
// 2. contramap(f ∘ g) = contramap(g) ∘ contramap(f)
|
||||
// - This is dual to the covariant functor (map) operation
|
||||
// - Consumers are contravariant because they consume rather than produce values
|
||||
//
|
||||
// Use Cases:
|
||||
// - Working with contravariant functors in a categorical style
|
||||
// - Adapting consumers to work with more specific types
|
||||
// - Building type-safe consumer transformations
|
||||
// - Implementing profunctor patterns (Consumer is a profunctor)
|
||||
func Contramap[R1, R2 any](f func(R2) R1) Operator[R1, R2] {
|
||||
return Local(f)
|
||||
}
|
||||
|
||||
@@ -381,3 +381,513 @@ func TestLocal(t *testing.T) {
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
}
|
||||
|
||||
func TestContramap(t *testing.T) {
|
||||
t.Run("basic contravariant mapping", func(t *testing.T) {
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
parseToInt := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
consumeString := Contramap(parseToInt)(consumeInt)
|
||||
consumeString("42")
|
||||
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
|
||||
t.Run("contravariant identity law", func(t *testing.T) {
|
||||
// contramap(identity) = identity
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
identity := function.Identity[int]
|
||||
consumeIdentity := Contramap(identity)(consumeInt)
|
||||
|
||||
consumeIdentity(42)
|
||||
assert.Equal(t, 42, captured)
|
||||
|
||||
// Should behave identically to original consumer
|
||||
consumeInt(100)
|
||||
capturedDirect := captured
|
||||
consumeIdentity(100)
|
||||
capturedMapped := captured
|
||||
|
||||
assert.Equal(t, capturedDirect, capturedMapped)
|
||||
})
|
||||
|
||||
t.Run("contravariant composition law", func(t *testing.T) {
|
||||
// contramap(f . g) = contramap(g) . contramap(f)
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
f := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
g := func(b bool) string {
|
||||
if b {
|
||||
return "1"
|
||||
}
|
||||
return "0"
|
||||
}
|
||||
|
||||
// Compose f and g manually
|
||||
fg := func(b bool) int {
|
||||
return f(g(b))
|
||||
}
|
||||
|
||||
// Method 1: contramap(f . g)
|
||||
consumer1 := Contramap(fg)(consumeInt)
|
||||
consumer1(true)
|
||||
result1 := captured
|
||||
|
||||
// Method 2: contramap(g) . contramap(f)
|
||||
consumer2 := Contramap(g)(Contramap(f)(consumeInt))
|
||||
consumer2(true)
|
||||
result2 := captured
|
||||
|
||||
assert.Equal(t, result1, result2)
|
||||
assert.Equal(t, 1, result1)
|
||||
})
|
||||
|
||||
t.Run("type hierarchy adaptation", func(t *testing.T) {
|
||||
type Animal struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
type Dog struct {
|
||||
Animal Animal
|
||||
Breed string
|
||||
}
|
||||
|
||||
var capturedName string
|
||||
consumeAnimal := func(a Animal) {
|
||||
capturedName = a.Name
|
||||
}
|
||||
|
||||
dogToAnimal := func(d Dog) Animal {
|
||||
return d.Animal
|
||||
}
|
||||
|
||||
consumeDog := Contramap(dogToAnimal)(consumeAnimal)
|
||||
consumeDog(Dog{
|
||||
Animal: Animal{Name: "Buddy"},
|
||||
Breed: "Golden Retriever",
|
||||
})
|
||||
|
||||
assert.Equal(t, "Buddy", capturedName)
|
||||
})
|
||||
|
||||
t.Run("field extraction with contramap", func(t *testing.T) {
|
||||
type Message struct {
|
||||
Text string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
var capturedText string
|
||||
consumeString := func(s string) {
|
||||
capturedText = s
|
||||
}
|
||||
|
||||
extractText := func(m Message) string {
|
||||
return m.Text
|
||||
}
|
||||
|
||||
consumeMessage := Contramap(extractText)(consumeString)
|
||||
consumeMessage(Message{
|
||||
Text: "Hello",
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
|
||||
assert.Equal(t, "Hello", capturedText)
|
||||
})
|
||||
|
||||
t.Run("multiple contramap applications", func(t *testing.T) {
|
||||
type Level3 struct{ Value int }
|
||||
type Level2 struct{ L3 Level3 }
|
||||
type Level1 struct{ L2 Level2 }
|
||||
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
extract3 := func(l3 Level3) int { return l3.Value }
|
||||
extract2 := func(l2 Level2) Level3 { return l2.L3 }
|
||||
extract1 := func(l1 Level1) Level2 { return l1.L2 }
|
||||
|
||||
// Chain contramap operations
|
||||
consumeLevel3 := Contramap(extract3)(consumeInt)
|
||||
consumeLevel2 := Contramap(extract2)(consumeLevel3)
|
||||
consumeLevel1 := Contramap(extract1)(consumeLevel2)
|
||||
|
||||
consumeLevel1(Level1{L2: Level2{L3: Level3{Value: 42}}})
|
||||
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
|
||||
t.Run("contramap with calculation", func(t *testing.T) {
|
||||
type Rectangle struct {
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
var capturedArea int
|
||||
consumeArea := func(area int) {
|
||||
capturedArea = area
|
||||
}
|
||||
|
||||
calculateArea := func(r Rectangle) int {
|
||||
return r.Width * r.Height
|
||||
}
|
||||
|
||||
consumeRectangle := Contramap(calculateArea)(consumeArea)
|
||||
consumeRectangle(Rectangle{Width: 5, Height: 10})
|
||||
|
||||
assert.Equal(t, 50, capturedArea)
|
||||
})
|
||||
|
||||
t.Run("contramap preserves side effects", func(t *testing.T) {
|
||||
callCount := 0
|
||||
consumer := func(x int) {
|
||||
callCount++
|
||||
}
|
||||
|
||||
transform := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
contramappedConsumer := Contramap(transform)(consumer)
|
||||
|
||||
contramappedConsumer("1")
|
||||
contramappedConsumer("2")
|
||||
contramappedConsumer("3")
|
||||
|
||||
assert.Equal(t, 3, callCount)
|
||||
})
|
||||
|
||||
t.Run("contramap with pointer types", func(t *testing.T) {
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
dereference := func(p *int) int {
|
||||
if p == nil {
|
||||
return 0
|
||||
}
|
||||
return *p
|
||||
}
|
||||
|
||||
consumePointer := Contramap(dereference)(consumeInt)
|
||||
|
||||
value := 42
|
||||
consumePointer(&value)
|
||||
assert.Equal(t, 42, captured)
|
||||
|
||||
consumePointer(nil)
|
||||
assert.Equal(t, 0, captured)
|
||||
})
|
||||
|
||||
t.Run("contramap equivalence with Local", func(t *testing.T) {
|
||||
var capturedLocal, capturedContramap int
|
||||
|
||||
consumeIntLocal := func(x int) {
|
||||
capturedLocal = x
|
||||
}
|
||||
|
||||
consumeIntContramap := func(x int) {
|
||||
capturedContramap = x
|
||||
}
|
||||
|
||||
transform := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
// Both should produce identical results
|
||||
consumerLocal := Local(transform)(consumeIntLocal)
|
||||
consumerContramap := Contramap(transform)(consumeIntContramap)
|
||||
|
||||
consumerLocal("42")
|
||||
consumerContramap("42")
|
||||
|
||||
assert.Equal(t, capturedLocal, capturedContramap)
|
||||
assert.Equal(t, 42, capturedLocal)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompose(t *testing.T) {
|
||||
t.Run("basic composition", func(t *testing.T) {
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
parseToInt := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
consumeString := Compose(parseToInt)(consumeInt)
|
||||
consumeString("42")
|
||||
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
|
||||
t.Run("composing multiple transformations", func(t *testing.T) {
|
||||
type Data struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
type Wrapper struct {
|
||||
Data Data
|
||||
}
|
||||
|
||||
var captured string
|
||||
consumeString := func(s string) {
|
||||
captured = s
|
||||
}
|
||||
|
||||
extractData := func(w Wrapper) Data { return w.Data }
|
||||
extractValue := func(d Data) string { return d.Value }
|
||||
|
||||
// Compose step by step
|
||||
consumeData := Compose(extractValue)(consumeString)
|
||||
consumeWrapper := Compose(extractData)(consumeData)
|
||||
|
||||
consumeWrapper(Wrapper{Data: Data{Value: "Hello"}})
|
||||
|
||||
assert.Equal(t, "Hello", captured)
|
||||
})
|
||||
|
||||
t.Run("function composition style", func(t *testing.T) {
|
||||
type Request struct {
|
||||
Body []byte
|
||||
}
|
||||
|
||||
var captured string
|
||||
processString := func(s string) {
|
||||
captured = s
|
||||
}
|
||||
|
||||
bytesToString := func(b []byte) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
extractBody := func(r Request) []byte {
|
||||
return r.Body
|
||||
}
|
||||
|
||||
// Chain compositions
|
||||
processBytes := Compose(bytesToString)(processString)
|
||||
processRequest := Compose(extractBody)(processBytes)
|
||||
|
||||
processRequest(Request{Body: []byte("test")})
|
||||
|
||||
assert.Equal(t, "test", captured)
|
||||
})
|
||||
|
||||
t.Run("compose with identity", func(t *testing.T) {
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
identity := function.Identity[int]
|
||||
composedConsumer := Compose(identity)(consumeInt)
|
||||
|
||||
composedConsumer(42)
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
|
||||
t.Run("compose with field extraction", func(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Email string
|
||||
Age int
|
||||
}
|
||||
|
||||
var capturedName string
|
||||
consumeName := func(name string) {
|
||||
capturedName = name
|
||||
}
|
||||
|
||||
extractName := func(u User) string {
|
||||
return u.Name
|
||||
}
|
||||
|
||||
consumeUser := Compose(extractName)(consumeName)
|
||||
consumeUser(User{Name: "Alice", Email: "alice@example.com", Age: 30})
|
||||
|
||||
assert.Equal(t, "Alice", capturedName)
|
||||
})
|
||||
|
||||
t.Run("compose with calculation", func(t *testing.T) {
|
||||
type Circle struct {
|
||||
Radius float64
|
||||
}
|
||||
|
||||
var capturedArea float64
|
||||
consumeArea := func(area float64) {
|
||||
capturedArea = area
|
||||
}
|
||||
|
||||
calculateArea := func(c Circle) float64 {
|
||||
return 3.14159 * c.Radius * c.Radius
|
||||
}
|
||||
|
||||
consumeCircle := Compose(calculateArea)(consumeArea)
|
||||
consumeCircle(Circle{Radius: 5.0})
|
||||
|
||||
assert.InDelta(t, 78.53975, capturedArea, 0.00001)
|
||||
})
|
||||
|
||||
t.Run("compose with slice operations", func(t *testing.T) {
|
||||
var captured int
|
||||
consumeLength := func(n int) {
|
||||
captured = n
|
||||
}
|
||||
|
||||
getLength := func(s []string) int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
consumeSlice := Compose(getLength)(consumeLength)
|
||||
consumeSlice([]string{"a", "b", "c", "d"})
|
||||
|
||||
assert.Equal(t, 4, captured)
|
||||
})
|
||||
|
||||
t.Run("compose with map operations", func(t *testing.T) {
|
||||
var captured bool
|
||||
consumeHasKey := func(has bool) {
|
||||
captured = has
|
||||
}
|
||||
|
||||
hasKey := func(m map[string]int) bool {
|
||||
_, exists := m["key"]
|
||||
return exists
|
||||
}
|
||||
|
||||
consumeMap := Compose(hasKey)(consumeHasKey)
|
||||
|
||||
consumeMap(map[string]int{"key": 42})
|
||||
assert.True(t, captured)
|
||||
|
||||
consumeMap(map[string]int{"other": 42})
|
||||
assert.False(t, captured)
|
||||
})
|
||||
|
||||
t.Run("compose preserves consumer behavior", func(t *testing.T) {
|
||||
callCount := 0
|
||||
consumer := func(x int) {
|
||||
callCount++
|
||||
}
|
||||
|
||||
transform := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
composedConsumer := Compose(transform)(consumer)
|
||||
|
||||
composedConsumer("1")
|
||||
composedConsumer("2")
|
||||
composedConsumer("3")
|
||||
|
||||
assert.Equal(t, 3, callCount)
|
||||
})
|
||||
|
||||
t.Run("compose with error handling", func(t *testing.T) {
|
||||
type Result struct {
|
||||
Value int
|
||||
Error error
|
||||
}
|
||||
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
extractValue := func(r Result) int {
|
||||
if r.Error != nil {
|
||||
return -1
|
||||
}
|
||||
return r.Value
|
||||
}
|
||||
|
||||
consumeResult := Compose(extractValue)(consumeInt)
|
||||
|
||||
consumeResult(Result{Value: 42, Error: nil})
|
||||
assert.Equal(t, 42, captured)
|
||||
|
||||
consumeResult(Result{Value: 100, Error: assert.AnError})
|
||||
assert.Equal(t, -1, captured)
|
||||
})
|
||||
|
||||
t.Run("compose equivalence with Local", func(t *testing.T) {
|
||||
var capturedLocal, capturedCompose int
|
||||
|
||||
consumeIntLocal := func(x int) {
|
||||
capturedLocal = x
|
||||
}
|
||||
|
||||
consumeIntCompose := func(x int) {
|
||||
capturedCompose = x
|
||||
}
|
||||
|
||||
transform := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
// Both should produce identical results
|
||||
consumerLocal := Local(transform)(consumeIntLocal)
|
||||
consumerCompose := Compose(transform)(consumeIntCompose)
|
||||
|
||||
consumerLocal("42")
|
||||
consumerCompose("42")
|
||||
|
||||
assert.Equal(t, capturedLocal, capturedCompose)
|
||||
assert.Equal(t, 42, capturedLocal)
|
||||
})
|
||||
|
||||
t.Run("compose equivalence with Contramap", func(t *testing.T) {
|
||||
var capturedCompose, capturedContramap int
|
||||
|
||||
consumeIntCompose := func(x int) {
|
||||
capturedCompose = x
|
||||
}
|
||||
|
||||
consumeIntContramap := func(x int) {
|
||||
capturedContramap = x
|
||||
}
|
||||
|
||||
transform := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
// All three should produce identical results
|
||||
consumerCompose := Compose(transform)(consumeIntCompose)
|
||||
consumerContramap := Contramap(transform)(consumeIntContramap)
|
||||
|
||||
consumerCompose("42")
|
||||
consumerContramap("42")
|
||||
|
||||
assert.Equal(t, capturedCompose, capturedContramap)
|
||||
assert.Equal(t, 42, capturedCompose)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ import (
|
||||
// return result.Of("done")
|
||||
// }
|
||||
//
|
||||
// ctx, cancel := context.WithCancel(context.Background())
|
||||
// ctx, cancel := context.WithCancel(t.Context())
|
||||
// cancel() // Cancel immediately
|
||||
//
|
||||
// wrapped := WithContext(ctx, computation)
|
||||
|
||||
@@ -61,7 +61,7 @@ import (
|
||||
//
|
||||
// // Safely read file with automatic cleanup
|
||||
// safeRead := Bracket(acquireFile, readFile, closeFile)
|
||||
// result := safeRead(context.Background())()
|
||||
// result := safeRead(t.Context())()
|
||||
//
|
||||
//go:inline
|
||||
func Bracket[
|
||||
|
||||
@@ -50,7 +50,7 @@ import (
|
||||
// // Sequence it to apply Config first
|
||||
// sequenced := SequenceReader[Config, int](getMultiplier)
|
||||
// cfg := Config{Timeout: 30}
|
||||
// result := sequenced(cfg)(context.Background())() // Returns 60
|
||||
// result := sequenced(cfg)(t.Context())() // Returns 60
|
||||
//
|
||||
//go:inline
|
||||
func SequenceReader[R, A any](ma ReaderIO[Reader[R, A]]) Reader[R, ReaderIO[A]] {
|
||||
@@ -107,7 +107,7 @@ func SequenceReader[R, A any](ma ReaderIO[Reader[R, A]]) Reader[R, ReaderIO[A]]
|
||||
//
|
||||
// // Provide Config to get final result
|
||||
// cfg := Config{Multiplier: 5}
|
||||
// finalResult := result(cfg)(context.Background())() // Returns 50
|
||||
// finalResult := result(cfg)(t.Context())() // Returns 50
|
||||
//
|
||||
//go:inline
|
||||
func TraverseReader[R, A, B any](
|
||||
|
||||
@@ -81,7 +81,7 @@ func SLogWithCallback[A any](
|
||||
// Chain(SLog[string]("Extracted name")),
|
||||
// )
|
||||
//
|
||||
// result := pipeline(context.Background())()
|
||||
// result := pipeline(t.Context())()
|
||||
// // Logs: "Fetched user" value={ID:123 Name:"Alice"}
|
||||
// // Logs: "Extracted name" value="Alice"
|
||||
//
|
||||
|
||||
@@ -45,7 +45,7 @@ func TestPromapBasic(t *testing.T) {
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap(addKey, toString)(getValue)
|
||||
result := adapted(context.Background())()
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.Equal(t, "42", result)
|
||||
})
|
||||
@@ -69,7 +69,7 @@ func TestContramapBasic(t *testing.T) {
|
||||
}
|
||||
|
||||
adapted := Contramap[int](addKey)(getValue)
|
||||
result := adapted(context.Background())()
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.Equal(t, 100, result)
|
||||
})
|
||||
@@ -90,7 +90,7 @@ func TestLocalBasic(t *testing.T) {
|
||||
}
|
||||
|
||||
adapted := Local[bool](addTimeout)(getValue)
|
||||
result := adapted(context.Background())()
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.True(t, result)
|
||||
})
|
||||
|
||||
@@ -594,7 +594,7 @@ func Read[A any](r context.Context) func(ReaderIO[A]) IO[A] {
|
||||
// )
|
||||
//
|
||||
// // Create context with side effects (e.g., loading config)
|
||||
// createContext := G.Of(context.WithValue(context.Background(), "key", "value"))
|
||||
// createContext := G.Of(context.WithValue(t.Context(), "key", "value"))
|
||||
//
|
||||
// // A computation that uses the context
|
||||
// getValue := readerio.FromReader(func(ctx context.Context) string {
|
||||
@@ -664,7 +664,7 @@ func ReadIO[A any](r IO[context.Context]) func(ReaderIO[A]) IO[A] {
|
||||
// getUser,
|
||||
// addUser,
|
||||
// )
|
||||
// user := result(context.Background())() // Returns "Alice"
|
||||
// user := result(t.Context())() // Returns "Alice"
|
||||
//
|
||||
// Timeout Example:
|
||||
//
|
||||
@@ -731,7 +731,7 @@ func Local[A any](f func(context.Context) (context.Context, context.CancelFunc))
|
||||
// fetchData,
|
||||
// readerio.WithTimeout[Data](5*time.Second),
|
||||
// )
|
||||
// data := result(context.Background())() // Returns Data{} after 5s timeout
|
||||
// data := result(t.Context())() // Returns Data{} after 5s timeout
|
||||
//
|
||||
// Successful Example:
|
||||
//
|
||||
@@ -740,7 +740,7 @@ func Local[A any](f func(context.Context) (context.Context, context.CancelFunc))
|
||||
// quickFetch,
|
||||
// readerio.WithTimeout[Data](5*time.Second),
|
||||
// )
|
||||
// data := result(context.Background())() // Returns Data{Value: "quick"}
|
||||
// data := result(t.Context())() // Returns Data{Value: "quick"}
|
||||
func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
|
||||
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(ctx, timeout)
|
||||
@@ -791,12 +791,12 @@ func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
|
||||
// fetchData,
|
||||
// readerio.WithDeadline[Data](deadline),
|
||||
// )
|
||||
// data := result(context.Background())() // Returns Data{} if past deadline
|
||||
// data := result(t.Context())() // Returns Data{} if past deadline
|
||||
//
|
||||
// Combining with Parent Context:
|
||||
//
|
||||
// // If parent context already has a deadline, the earlier one takes precedence
|
||||
// parentCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Hour))
|
||||
// parentCtx, cancel := context.WithDeadline(t.Context(), time.Now().Add(1*time.Hour))
|
||||
// defer cancel()
|
||||
//
|
||||
// laterDeadline := time.Now().Add(2 * time.Hour)
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestMonadMap(t *testing.T) {
|
||||
rio := Of(5)
|
||||
doubled := MonadMap(rio, N.Mul(2))
|
||||
|
||||
result := doubled(context.Background())()
|
||||
result := doubled(t.Context())()
|
||||
assert.Equal(t, 10, result)
|
||||
}
|
||||
|
||||
@@ -41,14 +41,14 @@ func TestMap(t *testing.T) {
|
||||
Map(utils.Double),
|
||||
)
|
||||
|
||||
assert.Equal(t, 2, g(context.Background())())
|
||||
assert.Equal(t, 2, g(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadMapTo(t *testing.T) {
|
||||
rio := Of(42)
|
||||
replaced := MonadMapTo(rio, "constant")
|
||||
|
||||
result := replaced(context.Background())()
|
||||
result := replaced(t.Context())()
|
||||
assert.Equal(t, "constant", result)
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ func TestMapTo(t *testing.T) {
|
||||
MapTo[int]("constant"),
|
||||
)
|
||||
|
||||
assert.Equal(t, "constant", result(context.Background())())
|
||||
assert.Equal(t, "constant", result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadChain(t *testing.T) {
|
||||
@@ -67,7 +67,7 @@ func TestMonadChain(t *testing.T) {
|
||||
return Of(n * 3)
|
||||
})
|
||||
|
||||
assert.Equal(t, 15, result(context.Background())())
|
||||
assert.Equal(t, 15, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestChain(t *testing.T) {
|
||||
@@ -78,7 +78,7 @@ func TestChain(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
assert.Equal(t, 15, result(context.Background())())
|
||||
assert.Equal(t, 15, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadChainFirst(t *testing.T) {
|
||||
@@ -89,7 +89,7 @@ func TestMonadChainFirst(t *testing.T) {
|
||||
return Of("side effect")
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -104,7 +104,7 @@ func TestChainFirst(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -117,7 +117,7 @@ func TestMonadTap(t *testing.T) {
|
||||
return Of(func() {})
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -132,14 +132,14 @@ func TestTap(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestOf(t *testing.T) {
|
||||
rio := Of(100)
|
||||
result := rio(context.Background())()
|
||||
result := rio(t.Context())()
|
||||
|
||||
assert.Equal(t, 100, result)
|
||||
}
|
||||
@@ -149,7 +149,7 @@ func TestMonadAp(t *testing.T) {
|
||||
faIO := Of(5)
|
||||
result := MonadAp(fabIO, faIO)
|
||||
|
||||
assert.Equal(t, 10, result(context.Background())())
|
||||
assert.Equal(t, 10, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestAp(t *testing.T) {
|
||||
@@ -158,7 +158,7 @@ func TestAp(t *testing.T) {
|
||||
Ap[int](Of(1)),
|
||||
)
|
||||
|
||||
assert.Equal(t, 2, g(context.Background())())
|
||||
assert.Equal(t, 2, g(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadApSeq(t *testing.T) {
|
||||
@@ -166,7 +166,7 @@ func TestMonadApSeq(t *testing.T) {
|
||||
faIO := Of(5)
|
||||
result := MonadApSeq(fabIO, faIO)
|
||||
|
||||
assert.Equal(t, 15, result(context.Background())())
|
||||
assert.Equal(t, 15, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestApSeq(t *testing.T) {
|
||||
@@ -175,7 +175,7 @@ func TestApSeq(t *testing.T) {
|
||||
ApSeq[int](Of(5)),
|
||||
)
|
||||
|
||||
assert.Equal(t, 15, g(context.Background())())
|
||||
assert.Equal(t, 15, g(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadApPar(t *testing.T) {
|
||||
@@ -183,7 +183,7 @@ func TestMonadApPar(t *testing.T) {
|
||||
faIO := Of(5)
|
||||
result := MonadApPar(fabIO, faIO)
|
||||
|
||||
assert.Equal(t, 15, result(context.Background())())
|
||||
assert.Equal(t, 15, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestApPar(t *testing.T) {
|
||||
@@ -192,12 +192,12 @@ func TestApPar(t *testing.T) {
|
||||
ApPar[int](Of(5)),
|
||||
)
|
||||
|
||||
assert.Equal(t, 15, g(context.Background())())
|
||||
assert.Equal(t, 15, g(t.Context())())
|
||||
}
|
||||
|
||||
func TestAsk(t *testing.T) {
|
||||
rio := Ask()
|
||||
ctx := context.WithValue(context.Background(), "key", "value")
|
||||
ctx := context.WithValue(t.Context(), "key", "value")
|
||||
result := rio(ctx)()
|
||||
|
||||
assert.Equal(t, ctx, result)
|
||||
@@ -207,7 +207,7 @@ func TestFromIO(t *testing.T) {
|
||||
ioAction := G.Of(42)
|
||||
rio := FromIO(ioAction)
|
||||
|
||||
result := rio(context.Background())()
|
||||
result := rio(t.Context())()
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ func TestFromReader(t *testing.T) {
|
||||
}
|
||||
|
||||
rio := FromReader(rdr)
|
||||
result := rio(context.Background())()
|
||||
result := rio(t.Context())()
|
||||
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
@@ -226,7 +226,7 @@ func TestFromLazy(t *testing.T) {
|
||||
lazy := func() int { return 42 }
|
||||
rio := FromLazy(lazy)
|
||||
|
||||
result := rio(context.Background())()
|
||||
result := rio(t.Context())()
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
@@ -236,7 +236,7 @@ func TestMonadChainIOK(t *testing.T) {
|
||||
return G.Of(n * 4)
|
||||
})
|
||||
|
||||
assert.Equal(t, 20, result(context.Background())())
|
||||
assert.Equal(t, 20, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestChainIOK(t *testing.T) {
|
||||
@@ -247,7 +247,7 @@ func TestChainIOK(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
assert.Equal(t, 20, result(context.Background())())
|
||||
assert.Equal(t, 20, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadChainFirstIOK(t *testing.T) {
|
||||
@@ -258,7 +258,7 @@ func TestMonadChainFirstIOK(t *testing.T) {
|
||||
return G.Of("side effect")
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -273,7 +273,7 @@ func TestChainFirstIOK(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -286,7 +286,7 @@ func TestMonadTapIOK(t *testing.T) {
|
||||
return G.Of(func() {})
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -301,7 +301,7 @@ func TestTapIOK(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -313,8 +313,8 @@ func TestDefer(t *testing.T) {
|
||||
return Of(counter)
|
||||
})
|
||||
|
||||
result1 := rio(context.Background())()
|
||||
result2 := rio(context.Background())()
|
||||
result1 := rio(t.Context())()
|
||||
result2 := rio(t.Context())()
|
||||
|
||||
assert.Equal(t, 1, result1)
|
||||
assert.Equal(t, 2, result2)
|
||||
@@ -328,8 +328,8 @@ func TestMemoize(t *testing.T) {
|
||||
return counter
|
||||
}))
|
||||
|
||||
result1 := memoized(context.Background())()
|
||||
result2 := memoized(context.Background())()
|
||||
result1 := memoized(t.Context())()
|
||||
result2 := memoized(t.Context())()
|
||||
|
||||
assert.Equal(t, 1, result1)
|
||||
assert.Equal(t, 1, result2) // Same value, memoized
|
||||
@@ -339,7 +339,7 @@ func TestFlatten(t *testing.T) {
|
||||
nested := Of(Of(42))
|
||||
flattened := Flatten(nested)
|
||||
|
||||
result := flattened(context.Background())()
|
||||
result := flattened(t.Context())()
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
@@ -347,7 +347,7 @@ func TestMonadFlap(t *testing.T) {
|
||||
fabIO := Of(N.Mul(3))
|
||||
result := MonadFlap(fabIO, 7)
|
||||
|
||||
assert.Equal(t, 21, result(context.Background())())
|
||||
assert.Equal(t, 21, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestFlap(t *testing.T) {
|
||||
@@ -356,7 +356,7 @@ func TestFlap(t *testing.T) {
|
||||
Flap[int](7),
|
||||
)
|
||||
|
||||
assert.Equal(t, 21, result(context.Background())())
|
||||
assert.Equal(t, 21, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadChainReaderK(t *testing.T) {
|
||||
@@ -365,7 +365,7 @@ func TestMonadChainReaderK(t *testing.T) {
|
||||
return func(ctx context.Context) int { return n * 2 }
|
||||
})
|
||||
|
||||
assert.Equal(t, 10, result(context.Background())())
|
||||
assert.Equal(t, 10, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestChainReaderK(t *testing.T) {
|
||||
@@ -376,7 +376,7 @@ func TestChainReaderK(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
assert.Equal(t, 10, result(context.Background())())
|
||||
assert.Equal(t, 10, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadChainFirstReaderK(t *testing.T) {
|
||||
@@ -389,7 +389,7 @@ func TestMonadChainFirstReaderK(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -406,7 +406,7 @@ func TestChainFirstReaderK(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -421,7 +421,7 @@ func TestMonadTapReaderK(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -438,14 +438,14 @@ func TestTapReaderK(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
rio := Of(42)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
ioAction := Read[int](ctx)(rio)
|
||||
result := ioAction()
|
||||
|
||||
@@ -463,7 +463,7 @@ func TestComplexPipeline(t *testing.T) {
|
||||
Map(N.Add(10)),
|
||||
)
|
||||
|
||||
assert.Equal(t, 20, result(context.Background())()) // (5 * 2) + 10 = 20
|
||||
assert.Equal(t, 20, result(t.Context())()) // (5 * 2) + 10 = 20
|
||||
}
|
||||
|
||||
func TestFromIOWithChain(t *testing.T) {
|
||||
@@ -476,7 +476,7 @@ func TestFromIOWithChain(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
assert.Equal(t, 15, result(context.Background())())
|
||||
assert.Equal(t, 15, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestTapWithLogging(t *testing.T) {
|
||||
@@ -496,14 +496,14 @@ func TestTapWithLogging(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 84, value)
|
||||
assert.Equal(t, []int{42, 84}, logged)
|
||||
}
|
||||
|
||||
func TestReadIO(t *testing.T) {
|
||||
// Test basic ReadIO functionality
|
||||
contextIO := G.Of(context.WithValue(context.Background(), "testKey", "testValue"))
|
||||
contextIO := G.Of(context.WithValue(t.Context(), "testKey", "testValue"))
|
||||
rio := FromReader(func(ctx context.Context) string {
|
||||
if val := ctx.Value("testKey"); val != nil {
|
||||
return val.(string)
|
||||
@@ -519,7 +519,7 @@ func TestReadIO(t *testing.T) {
|
||||
|
||||
func TestReadIOWithBackground(t *testing.T) {
|
||||
// Test ReadIO with plain background context
|
||||
contextIO := G.Of(context.Background())
|
||||
contextIO := G.Of(t.Context())
|
||||
rio := Of(42)
|
||||
|
||||
ioAction := ReadIO[int](contextIO)(rio)
|
||||
@@ -530,7 +530,7 @@ func TestReadIOWithBackground(t *testing.T) {
|
||||
|
||||
func TestReadIOWithChain(t *testing.T) {
|
||||
// Test ReadIO with chained operations
|
||||
contextIO := G.Of(context.WithValue(context.Background(), "multiplier", 3))
|
||||
contextIO := G.Of(context.WithValue(t.Context(), "multiplier", 3))
|
||||
|
||||
result := F.Pipe1(
|
||||
FromReader(func(ctx context.Context) int {
|
||||
@@ -552,7 +552,7 @@ func TestReadIOWithChain(t *testing.T) {
|
||||
|
||||
func TestReadIOWithMap(t *testing.T) {
|
||||
// Test ReadIO with Map operations
|
||||
contextIO := G.Of(context.Background())
|
||||
contextIO := G.Of(t.Context())
|
||||
|
||||
result := F.Pipe2(
|
||||
Of(5),
|
||||
@@ -571,7 +571,7 @@ func TestReadIOWithSideEffects(t *testing.T) {
|
||||
counter := 0
|
||||
contextIO := func() context.Context {
|
||||
counter++
|
||||
return context.WithValue(context.Background(), "counter", counter)
|
||||
return context.WithValue(t.Context(), "counter", counter)
|
||||
}
|
||||
|
||||
rio := FromReader(func(ctx context.Context) int {
|
||||
@@ -593,7 +593,7 @@ func TestReadIOMultipleExecutions(t *testing.T) {
|
||||
counter := 0
|
||||
contextIO := func() context.Context {
|
||||
counter++
|
||||
return context.Background()
|
||||
return t.Context()
|
||||
}
|
||||
|
||||
rio := Of(42)
|
||||
@@ -609,7 +609,7 @@ func TestReadIOMultipleExecutions(t *testing.T) {
|
||||
|
||||
func TestReadIOComparisonWithRead(t *testing.T) {
|
||||
// Compare ReadIO with Read to show the difference
|
||||
ctx := context.WithValue(context.Background(), "key", "value")
|
||||
ctx := context.WithValue(t.Context(), "key", "value")
|
||||
|
||||
rio := FromReader(func(ctx context.Context) string {
|
||||
if val := ctx.Value("key"); val != nil {
|
||||
@@ -642,7 +642,7 @@ func TestReadIOWithComplexContext(t *testing.T) {
|
||||
|
||||
contextIO := G.Of(
|
||||
context.WithValue(
|
||||
context.WithValue(context.Background(), userKey, "Alice"),
|
||||
context.WithValue(t.Context(), userKey, "Alice"),
|
||||
tokenKey,
|
||||
"secret123",
|
||||
),
|
||||
@@ -668,7 +668,7 @@ func TestReadIOWithComplexContext(t *testing.T) {
|
||||
|
||||
func TestReadIOWithAsk(t *testing.T) {
|
||||
// Test ReadIO combined with Ask
|
||||
contextIO := G.Of(context.WithValue(context.Background(), "data", 100))
|
||||
contextIO := G.Of(context.WithValue(t.Context(), "data", 100))
|
||||
|
||||
result := F.Pipe1(
|
||||
Ask(),
|
||||
|
||||
@@ -53,7 +53,7 @@ import (
|
||||
// }
|
||||
//
|
||||
// countdown := TailRec(countdownStep)
|
||||
// result := countdown(10)(context.Background())() // Returns "Done!"
|
||||
// result := countdown(10)(t.Context())() // Returns "Done!"
|
||||
//
|
||||
// Example - Sum with context:
|
||||
//
|
||||
@@ -77,7 +77,7 @@ import (
|
||||
// }
|
||||
//
|
||||
// sum := TailRec(sumStep)
|
||||
// result := sum(SumState{numbers: []int{1, 2, 3, 4, 5}})(context.Background())()
|
||||
// result := sum(SumState{numbers: []int{1, 2, 3, 4, 5}})(t.Context())()
|
||||
// // Returns 15, safe even for very large slices
|
||||
//
|
||||
//go:inline
|
||||
|
||||
@@ -80,7 +80,7 @@ import (
|
||||
// retryingFetch := Retrying(policy, fetchData, shouldRetry)
|
||||
//
|
||||
// // Execute
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// result := retryingFetch(ctx)() // Returns "success" after 3 attempts
|
||||
//
|
||||
//go:inline
|
||||
|
||||
@@ -56,7 +56,7 @@ This creates several problems:
|
||||
|
||||
```go
|
||||
computation := getComputation()
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
cfg := Config{Value: 42}
|
||||
|
||||
// Must apply in this specific order
|
||||
@@ -176,7 +176,7 @@ db := Database{ConnectionString: "localhost:5432"}
|
||||
query := queryWithDB(db) // ✅ Database injected
|
||||
|
||||
// Use query with any context
|
||||
result := query(context.Background())()
|
||||
result := query(t.Context())()
|
||||
```
|
||||
|
||||
### 3. Point-Free Composition
|
||||
@@ -289,7 +289,7 @@ withConfig := traversed(getValue)
|
||||
|
||||
// Now we can provide Config to get the final result
|
||||
cfg := Config{Multiplier: 5, Prefix: "Result"}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
result := withConfig(cfg)(ctx)() // Returns Right("Result: 50")
|
||||
```
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ func WithContext[A any](ma ReaderIOResult[A]) ReaderIOResult[A] {
|
||||
// safeFetch := WithContextK(fetchUser)
|
||||
//
|
||||
// // If context is cancelled, returns immediately without executing fetchUser
|
||||
// ctx, cancel := context.WithCancel(context.Background())
|
||||
// ctx, cancel := context.WithCancel(t.Context())
|
||||
// cancel() // Cancel immediately
|
||||
// result := safeFetch(123)(ctx)() // Returns context.Canceled error
|
||||
//
|
||||
|
||||
@@ -187,7 +187,7 @@ func main() {
|
||||
result := cb(env)
|
||||
|
||||
// Execute the protected operation
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
protectedOp := pair.Tail(result)
|
||||
outcome := protectedOp(ctx)()
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
// }
|
||||
//
|
||||
// // Execute the computation
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// result := fetchUser("123")(ctx)()
|
||||
// // result is Either[error, User]
|
||||
//
|
||||
@@ -161,7 +161,7 @@
|
||||
// All operations respect context cancellation. When a context is cancelled, operations
|
||||
// will return an error containing the cancellation cause:
|
||||
//
|
||||
// ctx, cancel := context.WithCancelCause(context.Background())
|
||||
// ctx, cancel := context.WithCancelCause(t.Context())
|
||||
// cancel(errors.New("operation cancelled"))
|
||||
// result := computation(ctx)() // Returns Left with cancellation error
|
||||
//
|
||||
|
||||
@@ -37,7 +37,7 @@ import (
|
||||
// return either.Eq(eq.FromEquals(func(x, y int) bool { return x == y }))(a, b)
|
||||
// })
|
||||
// eqRIE := Eq(eqInt)
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// equal := eqRIE(ctx).Equals(Right[int](42), Right[int](42)) // true
|
||||
//
|
||||
//go:inline
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
@@ -30,7 +29,7 @@ func TestWithTempFile(t *testing.T) {
|
||||
|
||||
res := WithTempFile(onWriteAll[*os.File]([]byte("Carsten")))
|
||||
|
||||
assert.Equal(t, E.Of[error]([]byte("Carsten")), res(context.Background())())
|
||||
assert.Equal(t, E.Of[error]([]byte("Carsten")), res(t.Context())())
|
||||
}
|
||||
|
||||
func TestWithTempFileOnClosedFile(t *testing.T) {
|
||||
@@ -43,5 +42,5 @@ func TestWithTempFileOnClosedFile(t *testing.T) {
|
||||
)
|
||||
})
|
||||
|
||||
assert.Equal(t, E.Of[error]([]byte("Carsten")), res(context.Background())())
|
||||
assert.Equal(t, E.Of[error]([]byte("Carsten")), res(t.Context())())
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ import (
|
||||
// onNegative := func(n int) error { return fmt.Errorf("%d is not positive", n) }
|
||||
//
|
||||
// filter := readerioresult.FilterOrElse(isPositive, onNegative)
|
||||
// result := filter(readerioresult.Right(42))(context.Background())()
|
||||
// result := filter(readerioresult.Right(42))(t.Context())()
|
||||
//
|
||||
//go:inline
|
||||
func FilterOrElse[A any](pred Predicate[A], onFalse func(A) error) Operator[A, A] {
|
||||
|
||||
@@ -71,7 +71,7 @@ import (
|
||||
//
|
||||
// // Now we can partially apply the Config
|
||||
// cfg := Config{Timeout: 30}
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// result := sequenced(ctx)(cfg)() // Returns Right(60)
|
||||
//
|
||||
// This is especially useful in point-free style when building computation pipelines:
|
||||
@@ -133,7 +133,7 @@ func SequenceReader[R, A any](ma ReaderIOResult[Reader[R, A]]) Kleisli[R, A] {
|
||||
//
|
||||
// // Partially apply the Database
|
||||
// db := Database{ConnectionString: "localhost:5432"}
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// result := sequenced(ctx)(db)() // Executes IO and returns Right("Query result...")
|
||||
//
|
||||
// In point-free style, this enables clean composition:
|
||||
@@ -195,7 +195,7 @@ func SequenceReaderIO[R, A any](ma ReaderIOResult[RIO.ReaderIO[R, A]]) Kleisli[R
|
||||
//
|
||||
// // Partially apply the Config
|
||||
// cfg := Config{MaxRetries: 3}
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// result := sequenced(ctx)(cfg)() // Returns Right(3)
|
||||
//
|
||||
// // With invalid config
|
||||
@@ -276,7 +276,7 @@ func SequenceReaderResult[R, A any](ma ReaderIOResult[RR.ReaderResult[R, A]]) Kl
|
||||
//
|
||||
// // Now we can provide the Config to get the final result
|
||||
// cfg := Config{Multiplier: 5}
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// finalResult := result(cfg)(ctx)() // Returns Right(50)
|
||||
//
|
||||
// In point-free style, this enables clean composition:
|
||||
|
||||
@@ -41,7 +41,7 @@ func TestSequenceReader(t *testing.T) {
|
||||
// The Reader environment (string) is now the first parameter
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test original
|
||||
result1 := original(ctx)()
|
||||
@@ -75,7 +75,7 @@ func TestSequenceReader(t *testing.T) {
|
||||
}
|
||||
|
||||
db := Database{ConnectionString: "localhost:5432"}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
expected := "Query on localhost:5432"
|
||||
|
||||
@@ -106,7 +106,7 @@ func TestSequenceReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test original with error
|
||||
result1 := original(ctx)()
|
||||
@@ -132,7 +132,7 @@ func TestSequenceReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Sequence
|
||||
sequenced := SequenceReader(original)
|
||||
@@ -158,7 +158,7 @@ func TestSequenceReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
// Test with zero values
|
||||
@@ -184,7 +184,7 @@ func TestSequenceReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
sequenced := SequenceReader(original)
|
||||
@@ -217,14 +217,14 @@ func TestSequenceReader(t *testing.T) {
|
||||
withConfig := sequenced(cfg)
|
||||
|
||||
// Now we have a ReaderIOResult[int] that can be used in different contexts
|
||||
ctx1 := context.Background()
|
||||
ctx1 := t.Context()
|
||||
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()
|
||||
ctx2 := t.Context()
|
||||
result2 := withConfig(ctx2)()
|
||||
assert.True(t, either.IsRight(result2))
|
||||
value2, _ := either.Unwrap(result2)
|
||||
@@ -246,7 +246,7 @@ func TestSequenceReaderIO(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
sequenced := SequenceReaderIO(original)
|
||||
|
||||
// Test original
|
||||
@@ -273,7 +273,7 @@ func TestSequenceReaderIO(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test original with error
|
||||
result1 := original(ctx)()
|
||||
@@ -303,7 +303,7 @@ func TestSequenceReaderIO(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
sequenced := SequenceReaderIO(original)
|
||||
@@ -327,7 +327,7 @@ func TestSequenceReaderResult(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
sequenced := SequenceReaderResult(original)
|
||||
|
||||
// Test original
|
||||
@@ -356,7 +356,7 @@ func TestSequenceReaderResult(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test original with error
|
||||
result1 := original(ctx)()
|
||||
@@ -384,7 +384,7 @@ func TestSequenceReaderResult(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test original with inner error
|
||||
result1 := original(ctx)()
|
||||
@@ -421,7 +421,7 @@ func TestSequenceReaderResult(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test outer error
|
||||
sequenced1 := SequenceReaderResult(makeOriginal(-20))
|
||||
@@ -460,7 +460,7 @@ func TestSequenceReaderResult(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
sequenced := SequenceReaderResult(original)
|
||||
@@ -484,7 +484,7 @@ func TestSequenceEdgeCases(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
empty := Empty{}
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
@@ -514,7 +514,7 @@ func TestSequenceEdgeCases(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
data := &Data{Value: 100}
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
@@ -544,7 +544,7 @@ func TestSequenceEdgeCases(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
// Call multiple times with same inputs
|
||||
@@ -583,7 +583,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
|
||||
// Provide Config and execute
|
||||
cfg := Config{Multiplier: 5}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
finalResult := result(cfg)(ctx)()
|
||||
|
||||
assert.True(t, either.IsRight(finalResult))
|
||||
@@ -614,7 +614,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
|
||||
// Provide Config and execute
|
||||
cfg := Config{Multiplier: 5}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
finalResult := result(cfg)(ctx)()
|
||||
|
||||
assert.True(t, either.IsLeft(finalResult))
|
||||
@@ -643,7 +643,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
|
||||
// Provide Database and execute
|
||||
db := Database{Prefix: "ID"}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
finalResult := result(db)(ctx)()
|
||||
|
||||
assert.True(t, either.IsRight(finalResult))
|
||||
@@ -673,7 +673,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
|
||||
// Provide Settings and execute
|
||||
settings := Settings{Prefix: "[", Suffix: "]"}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
finalResult := result(settings)(ctx)()
|
||||
|
||||
assert.True(t, either.IsRight(finalResult))
|
||||
@@ -705,14 +705,14 @@ func TestTraverseReader(t *testing.T) {
|
||||
withConfig := result(cfg)
|
||||
|
||||
// Can now use with different contexts
|
||||
ctx1 := context.Background()
|
||||
ctx1 := t.Context()
|
||||
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()
|
||||
ctx2 := t.Context()
|
||||
finalResult2 := withConfig(ctx2)()
|
||||
assert.True(t, either.IsRight(finalResult2))
|
||||
value2, _ := either.Unwrap(finalResult2)
|
||||
@@ -746,7 +746,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
result := traversed(original)
|
||||
|
||||
// Use canceled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
cfg := Config{Value: 5}
|
||||
@@ -778,7 +778,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
|
||||
// Provide Config with zero offset
|
||||
cfg := Config{Offset: 0}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
finalResult := result(cfg)(ctx)()
|
||||
|
||||
assert.True(t, either.IsRight(finalResult))
|
||||
@@ -807,7 +807,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
|
||||
// Provide Config and execute
|
||||
cfg := Config{Multiplier: 4}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
finalResult := result(cfg)(ctx)()
|
||||
|
||||
assert.True(t, either.IsRight(finalResult))
|
||||
@@ -843,7 +843,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
|
||||
// Test with value within range
|
||||
rules1 := ValidationRules{MinValue: 0, MaxValue: 100}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
finalResult1 := result(rules1)(ctx)()
|
||||
assert.True(t, either.IsRight(finalResult1))
|
||||
value1, _ := either.Unwrap(finalResult1)
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
// )
|
||||
//
|
||||
// requester := RB.Requester(builder)
|
||||
// result := requester(context.Background())()
|
||||
// result := requester(t.Context())()
|
||||
package builder
|
||||
|
||||
import (
|
||||
@@ -103,7 +103,7 @@ import (
|
||||
// B.WithJSONBody(map[string]string{"name": "John"}),
|
||||
// )
|
||||
// requester := RB.Requester(builder)
|
||||
// result := requester(context.Background())()
|
||||
// result := requester(t.Context())()
|
||||
//
|
||||
// Example without body:
|
||||
//
|
||||
@@ -113,7 +113,7 @@ import (
|
||||
// B.WithMethod("GET"),
|
||||
// )
|
||||
// requester := RB.Requester(builder)
|
||||
// result := requester(context.Background())()
|
||||
// result := requester(t.Context())()
|
||||
func Requester(builder *R.Builder) RIOEH.Requester {
|
||||
|
||||
withBody := F.Curry3(func(data []byte, url string, method string) RIOE.ReaderIOResult[*http.Request] {
|
||||
|
||||
@@ -55,7 +55,7 @@ func TestBuilderWithQuery(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
assert.True(t, E.IsRight(req(context.Background())()))
|
||||
assert.True(t, E.IsRight(req(t.Context())()))
|
||||
}
|
||||
|
||||
// TestBuilderWithoutBody tests creating a request without a body
|
||||
@@ -67,7 +67,7 @@ func TestBuilderWithoutBody(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
@@ -90,7 +90,7 @@ func TestBuilderWithBody(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
@@ -112,7 +112,7 @@ func TestBuilderWithHeaders(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
@@ -130,7 +130,7 @@ func TestBuilderWithInvalidURL(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
assert.True(t, E.IsLeft(result), "Expected Left result for invalid URL")
|
||||
}
|
||||
@@ -144,7 +144,7 @@ func TestBuilderWithEmptyMethod(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
// Empty method should still work (defaults to GET in http.NewRequest)
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
@@ -161,7 +161,7 @@ func TestBuilderWithMultipleHeaders(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
@@ -185,7 +185,7 @@ func TestBuilderWithBodyAndHeaders(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
@@ -207,7 +207,7 @@ func TestBuilderContextCancellation(t *testing.T) {
|
||||
requester := Requester(builder)
|
||||
|
||||
// Create a cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel() // Cancel immediately
|
||||
|
||||
result := requester(ctx)()
|
||||
@@ -233,7 +233,7 @@ func TestBuilderWithDifferentMethods(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result for method %s", method)
|
||||
|
||||
@@ -256,7 +256,7 @@ func TestBuilderWithJSON(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
@@ -277,7 +277,7 @@ func TestBuilderWithBearer(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/data")
|
||||
// result := ReadJSON[MyType](client)(request)
|
||||
// response := result(context.Background())()
|
||||
// response := result(t.Context())()
|
||||
package http
|
||||
|
||||
import (
|
||||
@@ -157,7 +157,7 @@ func MakeClient(httpClient *http.Client) Client {
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/data")
|
||||
// fullResp := ReadFullResponse(client)(request)
|
||||
// result := fullResp(context.Background())()
|
||||
// result := fullResp(t.Context())()
|
||||
func ReadFullResponse(client Client) RIOE.Kleisli[Requester, H.FullResponse] {
|
||||
return func(req Requester) RIOE.ReaderIOResult[H.FullResponse] {
|
||||
return F.Flow3(
|
||||
@@ -194,7 +194,7 @@ func ReadFullResponse(client Client) RIOE.Kleisli[Requester, H.FullResponse] {
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/data")
|
||||
// readBytes := ReadAll(client)
|
||||
// result := readBytes(request)(context.Background())()
|
||||
// result := readBytes(request)(t.Context())()
|
||||
func ReadAll(client Client) RIOE.Kleisli[Requester, []byte] {
|
||||
return F.Flow2(
|
||||
ReadFullResponse(client),
|
||||
@@ -218,7 +218,7 @@ func ReadAll(client Client) RIOE.Kleisli[Requester, []byte] {
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/text")
|
||||
// readText := ReadText(client)
|
||||
// result := readText(request)(context.Background())()
|
||||
// result := readText(request)(t.Context())()
|
||||
func ReadText(client Client) RIOE.Kleisli[Requester, string] {
|
||||
return F.Flow2(
|
||||
ReadAll(client),
|
||||
@@ -277,7 +277,7 @@ func readJSON(client Client) RIOE.Kleisli[Requester, []byte] {
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/user/1")
|
||||
// readUser := ReadJSON[User](client)
|
||||
// result := readUser(request)(context.Background())()
|
||||
// result := readUser(request)(t.Context())()
|
||||
func ReadJSON[A any](client Client) RIOE.Kleisli[Requester, A] {
|
||||
return F.Flow2(
|
||||
readJSON(client),
|
||||
|
||||
@@ -429,7 +429,7 @@ func LogEntryExitWithCallback[A any](
|
||||
// loggedFetch := LogEntryExit[User]("fetchUser")(fetchUser(123))
|
||||
//
|
||||
// // Execute
|
||||
// result := loggedFetch(context.Background())()
|
||||
// result := loggedFetch(t.Context())()
|
||||
// // Logs:
|
||||
// // [entering 1] fetchUser
|
||||
// // [exiting 1] fetchUser [0.1s]
|
||||
@@ -441,7 +441,7 @@ func LogEntryExitWithCallback[A any](
|
||||
// }
|
||||
//
|
||||
// logged := LogEntryExit[string]("failingOp")(failingOp())
|
||||
// result := logged(context.Background())()
|
||||
// result := logged(t.Context())()
|
||||
// // Logs:
|
||||
// // [entering 2] failingOp
|
||||
// // [throwing 2] failingOp [0.0s]: connection timeout
|
||||
@@ -461,7 +461,7 @@ func LogEntryExitWithCallback[A any](
|
||||
// LogEntryExit[[]Order]("fetchOrders"),
|
||||
// )
|
||||
//
|
||||
// result := pipeline(context.Background())()
|
||||
// result := pipeline(t.Context())()
|
||||
// // Logs:
|
||||
// // [entering 3] fetchUser
|
||||
// // [exiting 3] fetchUser [0.1s]
|
||||
@@ -474,8 +474,8 @@ func LogEntryExitWithCallback[A any](
|
||||
// op1 := LogEntryExit[Data]("operation1")(fetchData(1))
|
||||
// op2 := LogEntryExit[Data]("operation2")(fetchData(2))
|
||||
//
|
||||
// go op1(context.Background())()
|
||||
// go op2(context.Background())()
|
||||
// go op1(t.Context())()
|
||||
// go op2(t.Context())()
|
||||
// // Logs (order may vary):
|
||||
// // [entering 5] operation1
|
||||
// // [entering 6] operation2
|
||||
@@ -615,7 +615,7 @@ func SLogWithCallback[A any](
|
||||
// Map(func(u User) string { return u.Name }),
|
||||
// )
|
||||
//
|
||||
// result := pipeline(context.Background())()
|
||||
// result := pipeline(t.Context())()
|
||||
// // If successful, logs: "Fetched user" value={ID:123 Name:"Alice"}
|
||||
// // If error, logs: "Fetched user" error="user not found"
|
||||
//
|
||||
@@ -679,7 +679,7 @@ func SLog[A any](message string) Kleisli[Result[A], A] {
|
||||
// Map(func(u User) string { return u.Name }),
|
||||
// )
|
||||
//
|
||||
// result := pipeline(context.Background())()
|
||||
// result := pipeline(t.Context())()
|
||||
// // Logs: "Fetched user" value={ID:123 Name:"Alice"}
|
||||
// // Returns: result.Of("Alice")
|
||||
//
|
||||
@@ -694,7 +694,7 @@ func SLog[A any](message string) Kleisli[Result[A], A] {
|
||||
// TapSLog[Payment]("Payment processed"),
|
||||
// )
|
||||
//
|
||||
// result := processOrder(context.Background())()
|
||||
// result := processOrder(t.Context())()
|
||||
// // Logs each successful step with the intermediate values
|
||||
// // If any step fails, subsequent TapSLog calls don't log
|
||||
//
|
||||
|
||||
@@ -26,7 +26,7 @@ func TestLoggingContext(t *testing.T) {
|
||||
LogEntryExit[string]("TestLoggingContext2"),
|
||||
)
|
||||
|
||||
assert.Equal(t, result.Of("Sample"), data(context.Background())())
|
||||
assert.Equal(t, result.Of("Sample"), data(t.Context())())
|
||||
}
|
||||
|
||||
// TestLogEntryExitSuccess tests successful operation logging
|
||||
@@ -43,7 +43,7 @@ func TestLogEntryExitSuccess(t *testing.T) {
|
||||
LogEntryExit[string]("TestOperation"),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.Equal(t, result.Of("success value"), res)
|
||||
|
||||
@@ -70,7 +70,7 @@ func TestLogEntryExitError(t *testing.T) {
|
||||
LogEntryExit[string]("FailingOperation"),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
|
||||
@@ -105,7 +105,7 @@ func TestLogEntryExitNested(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
res := outerOp(context.Background())()
|
||||
res := outerOp(t.Context())()
|
||||
|
||||
assert.True(t, result.IsRight(res))
|
||||
|
||||
@@ -137,7 +137,7 @@ func TestLogEntryExitWithCallback(t *testing.T) {
|
||||
LogEntryExitWithCallback[int](slog.LevelDebug, customCallback, "DebugOperation"),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
|
||||
@@ -163,7 +163,7 @@ func TestLogEntryExitDisabled(t *testing.T) {
|
||||
LogEntryExit[string]("DisabledOperation"),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.True(t, result.IsRight(res))
|
||||
|
||||
@@ -197,7 +197,7 @@ func TestLogEntryExitF(t *testing.T) {
|
||||
LogEntryExitF(onEntry, onExit),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.True(t, result.IsRight(res))
|
||||
assert.Equal(t, 1, entryCount, "Entry callback should be called once")
|
||||
@@ -234,7 +234,7 @@ func TestLogEntryExitFWithError(t *testing.T) {
|
||||
LogEntryExitF(onEntry, onExit),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
assert.Equal(t, 1, entryCount, "Entry callback should be called once")
|
||||
@@ -257,7 +257,7 @@ func TestLoggingIDUniqueness(t *testing.T) {
|
||||
Of(i),
|
||||
LogEntryExit[int]("Operation"),
|
||||
)
|
||||
op(context.Background())()
|
||||
op(t.Context())()
|
||||
}
|
||||
|
||||
logOutput := buf.String()
|
||||
@@ -287,7 +287,7 @@ func TestLogEntryExitWithContextLogger(t *testing.T) {
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
|
||||
ctx := logging.WithLogger(contextLogger)(context.Background())
|
||||
ctx := logging.WithLogger(contextLogger)(t.Context())
|
||||
|
||||
operation := F.Pipe1(
|
||||
Of("context value"),
|
||||
@@ -326,7 +326,7 @@ func TestLogEntryExitTiming(t *testing.T) {
|
||||
LogEntryExit[string]("SlowOperation"),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.True(t, result.IsRight(res))
|
||||
|
||||
@@ -379,7 +379,7 @@ func TestLogEntryExitChainedOperations(t *testing.T) {
|
||||
)),
|
||||
)
|
||||
|
||||
res := pipeline(context.Background())()
|
||||
res := pipeline(t.Context())()
|
||||
|
||||
assert.Equal(t, result.Of("2"), res)
|
||||
|
||||
@@ -408,7 +408,7 @@ func TestTapSLog(t *testing.T) {
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.Equal(t, result.Of(84), res)
|
||||
|
||||
@@ -443,7 +443,7 @@ func TestTapSLogInPipeline(t *testing.T) {
|
||||
TapSLog[int]("Step 3: Final length"),
|
||||
)
|
||||
|
||||
res := pipeline(context.Background())()
|
||||
res := pipeline(t.Context())()
|
||||
|
||||
assert.Equal(t, result.Of(11), res)
|
||||
|
||||
@@ -472,7 +472,7 @@ func TestTapSLogWithError(t *testing.T) {
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
|
||||
res := pipeline(context.Background())()
|
||||
res := pipeline(t.Context())()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
|
||||
@@ -504,7 +504,7 @@ func TestTapSLogWithStruct(t *testing.T) {
|
||||
Map(func(u User) string { return u.Name }),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.Equal(t, result.Of("Alice"), res)
|
||||
|
||||
@@ -530,7 +530,7 @@ func TestTapSLogDisabled(t *testing.T) {
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.Equal(t, result.Of(84), res)
|
||||
|
||||
@@ -546,7 +546,7 @@ func TestTapSLogWithContextLogger(t *testing.T) {
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
|
||||
ctx := logging.WithLogger(contextLogger)(context.Background())
|
||||
ctx := logging.WithLogger(contextLogger)(t.Context())
|
||||
|
||||
operation := F.Pipe2(
|
||||
Of("test value"),
|
||||
@@ -572,7 +572,7 @@ func TestSLogLogsSuccessValue(t *testing.T) {
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Create a Result and log it
|
||||
res1 := result.Of(42)
|
||||
@@ -594,7 +594,7 @@ func TestSLogLogsErrorValue(t *testing.T) {
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
testErr := errors.New("test error")
|
||||
|
||||
// Create an error Result and log it
|
||||
@@ -620,7 +620,7 @@ func TestSLogWithCallbackCustomLevel(t *testing.T) {
|
||||
return logger
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Create a Result and log it with custom callback
|
||||
res1 := result.Of(42)
|
||||
@@ -645,7 +645,7 @@ func TestSLogWithCallbackLogsError(t *testing.T) {
|
||||
return logger
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
testErr := errors.New("warning error")
|
||||
|
||||
// Create an error Result and log it with custom callback
|
||||
|
||||
@@ -43,7 +43,7 @@ func TestPromapBasic(t *testing.T) {
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap(addKey, toString)(getValue)
|
||||
result := adapted(context.Background())()
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.Equal(t, R.Of("42"), result)
|
||||
})
|
||||
@@ -67,7 +67,7 @@ func TestContramapBasic(t *testing.T) {
|
||||
}
|
||||
|
||||
adapted := Contramap[int](addKey)(getValue)
|
||||
result := adapted(context.Background())()
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.Equal(t, R.Of(100), result)
|
||||
})
|
||||
@@ -91,7 +91,7 @@ func TestLocalBasic(t *testing.T) {
|
||||
}
|
||||
|
||||
adapted := Local[string](addUser)(getValue)
|
||||
result := adapted(context.Background())()
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.Equal(t, R.Of("Alice"), result)
|
||||
})
|
||||
|
||||
@@ -1041,7 +1041,7 @@ func TapLeftIOK[A, B any](f io.Kleisli[error, B]) Operator[A, A] {
|
||||
// getUser,
|
||||
// addUser,
|
||||
// )
|
||||
// value, err := result(context.Background())() // Returns ("Alice", nil)
|
||||
// value, err := result(t.Context())() // Returns ("Alice", nil)
|
||||
//
|
||||
// Timeout Example:
|
||||
//
|
||||
@@ -1112,7 +1112,7 @@ func Local[A any](f func(context.Context) (context.Context, context.CancelFunc))
|
||||
// fetchData,
|
||||
// readerioresult.WithTimeout[Data](5*time.Second),
|
||||
// )
|
||||
// value, err := result(context.Background())() // Returns (Data{}, context.DeadlineExceeded) after 5s
|
||||
// value, err := result(t.Context())() // Returns (Data{}, context.DeadlineExceeded) after 5s
|
||||
//
|
||||
// Successful Example:
|
||||
//
|
||||
@@ -1121,7 +1121,7 @@ func Local[A any](f func(context.Context) (context.Context, context.CancelFunc))
|
||||
// quickFetch,
|
||||
// readerioresult.WithTimeout[Data](5*time.Second),
|
||||
// )
|
||||
// value, err := result(context.Background())() // Returns (Data{Value: "quick"}, nil)
|
||||
// value, err := result(t.Context())() // Returns (Data{Value: "quick"}, nil)
|
||||
func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
|
||||
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(ctx, timeout)
|
||||
@@ -1173,12 +1173,12 @@ func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
|
||||
// fetchData,
|
||||
// readerioresult.WithDeadline[Data](deadline),
|
||||
// )
|
||||
// value, err := result(context.Background())() // Returns (Data{}, context.DeadlineExceeded) if past deadline
|
||||
// value, err := result(t.Context())() // Returns (Data{}, context.DeadlineExceeded) if past deadline
|
||||
//
|
||||
// Combining with Parent Context:
|
||||
//
|
||||
// // If parent context already has a deadline, the earlier one takes precedence
|
||||
// parentCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Hour))
|
||||
// parentCtx, cancel := context.WithDeadline(t.Context(), time.Now().Add(1*time.Hour))
|
||||
// defer cancel()
|
||||
//
|
||||
// laterDeadline := time.Now().Add(2 * time.Hour)
|
||||
|
||||
@@ -36,56 +36,56 @@ func TestFromEither(t *testing.T) {
|
||||
t.Run("Right value", func(t *testing.T) {
|
||||
either := E.Right[error]("success")
|
||||
result := FromEither(either)
|
||||
assert.Equal(t, E.Right[error]("success"), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error]("success"), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("Left value", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
either := E.Left[string](err)
|
||||
result := FromEither(either)
|
||||
assert.Equal(t, E.Left[string](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[string](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromResult(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
result := FromResult(E.Right[error](42))
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("Error", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
result := FromResult(E.Left[int](err))
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
func TestLeft(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
result := Left[string](err)
|
||||
assert.Equal(t, E.Left[string](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[string](err), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestRight(t *testing.T) {
|
||||
result := Right("success")
|
||||
assert.Equal(t, E.Right[error]("success"), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error]("success"), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestOf(t *testing.T) {
|
||||
result := Of(42)
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadMap(t *testing.T) {
|
||||
t.Run("Map over Right", func(t *testing.T) {
|
||||
result := MonadMap(Of(5), N.Mul(2))
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("Map over Left", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
result := MonadMap(Left[int](err), N.Mul(2))
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -93,34 +93,34 @@ func TestMap(t *testing.T) {
|
||||
t.Run("Map with success", func(t *testing.T) {
|
||||
mapper := Map(N.Mul(2))
|
||||
result := mapper(Of(5))
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("Map with error", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
mapper := Map(N.Mul(2))
|
||||
result := mapper(Left[int](err))
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadMapTo(t *testing.T) {
|
||||
t.Run("MapTo with success", func(t *testing.T) {
|
||||
result := MonadMapTo(Of("original"), 42)
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("MapTo with error", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
result := MonadMapTo(Left[string](err), 42)
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
func TestMapTo(t *testing.T) {
|
||||
mapper := MapTo[string](42)
|
||||
result := mapper(Of("original"))
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadChain(t *testing.T) {
|
||||
@@ -128,7 +128,7 @@ func TestMonadChain(t *testing.T) {
|
||||
result := MonadChain(Of(5), func(x int) ReaderIOResult[int] {
|
||||
return Of(x * 2)
|
||||
})
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("Chain with error in first", func(t *testing.T) {
|
||||
@@ -136,7 +136,7 @@ func TestMonadChain(t *testing.T) {
|
||||
result := MonadChain(Left[int](err), func(x int) ReaderIOResult[int] {
|
||||
return Of(x * 2)
|
||||
})
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("Chain with error in second", func(t *testing.T) {
|
||||
@@ -144,7 +144,7 @@ func TestMonadChain(t *testing.T) {
|
||||
result := MonadChain(Of(5), func(x int) ReaderIOResult[int] {
|
||||
return Left[int](err)
|
||||
})
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ func TestChain(t *testing.T) {
|
||||
return Of(x * 2)
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadChainFirst(t *testing.T) {
|
||||
@@ -161,7 +161,7 @@ func TestMonadChainFirst(t *testing.T) {
|
||||
result := MonadChainFirst(Of(5), func(x int) ReaderIOResult[string] {
|
||||
return Of("ignored")
|
||||
})
|
||||
assert.Equal(t, E.Right[error](5), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](5), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("ChainFirst propagates error from second", func(t *testing.T) {
|
||||
@@ -169,7 +169,7 @@ func TestMonadChainFirst(t *testing.T) {
|
||||
result := MonadChainFirst(Of(5), func(x int) ReaderIOResult[string] {
|
||||
return Left[string](err)
|
||||
})
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ func TestChainFirst(t *testing.T) {
|
||||
return Of("ignored")
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
assert.Equal(t, E.Right[error](5), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](5), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadApSeq(t *testing.T) {
|
||||
@@ -186,7 +186,7 @@ func TestMonadApSeq(t *testing.T) {
|
||||
fab := Of(N.Mul(2))
|
||||
fa := Of(5)
|
||||
result := MonadApSeq(fab, fa)
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("ApSeq with error in function", func(t *testing.T) {
|
||||
@@ -194,7 +194,7 @@ func TestMonadApSeq(t *testing.T) {
|
||||
fab := Left[func(int) int](err)
|
||||
fa := Of(5)
|
||||
result := MonadApSeq(fab, fa)
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("ApSeq with error in value", func(t *testing.T) {
|
||||
@@ -202,7 +202,7 @@ func TestMonadApSeq(t *testing.T) {
|
||||
fab := Of(N.Mul(2))
|
||||
fa := Left[int](err)
|
||||
result := MonadApSeq(fab, fa)
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ func TestApSeq(t *testing.T) {
|
||||
fa := Of(5)
|
||||
fab := Of(N.Mul(2))
|
||||
result := MonadApSeq(fab, fa)
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestApPar(t *testing.T) {
|
||||
@@ -218,11 +218,11 @@ func TestApPar(t *testing.T) {
|
||||
fa := Of(5)
|
||||
fab := Of(N.Mul(2))
|
||||
result := MonadApPar(fab, fa)
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("ApPar with cancelled context", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
fa := Of(5)
|
||||
fab := Of(N.Mul(2))
|
||||
@@ -239,7 +239,7 @@ func TestFromPredicate(t *testing.T) {
|
||||
func(x int) error { return fmt.Errorf("value %d is not positive", x) },
|
||||
)
|
||||
result := pred(5)
|
||||
assert.Equal(t, E.Right[error](5), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](5), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("Predicate false", func(t *testing.T) {
|
||||
@@ -248,7 +248,7 @@ func TestFromPredicate(t *testing.T) {
|
||||
func(x int) error { return fmt.Errorf("value %d is not positive", x) },
|
||||
)
|
||||
result := pred(-5)
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsLeft(res))
|
||||
})
|
||||
}
|
||||
@@ -259,7 +259,7 @@ func TestOrElse(t *testing.T) {
|
||||
return Of(42)
|
||||
})
|
||||
result := fallback(Of(10))
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("OrElse with error", func(t *testing.T) {
|
||||
@@ -268,13 +268,13 @@ func TestOrElse(t *testing.T) {
|
||||
return Of(42)
|
||||
})
|
||||
result := fallback(Left[int](err))
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
func TestAsk(t *testing.T) {
|
||||
result := Ask()
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
res := result(ctx)()
|
||||
assert.True(t, E.IsRight(res))
|
||||
ctxResult := E.ToOption(res)
|
||||
@@ -286,7 +286,7 @@ func TestMonadChainEitherK(t *testing.T) {
|
||||
result := MonadChainEitherK(Of(5), func(x int) Either[int] {
|
||||
return E.Right[error](x * 2)
|
||||
})
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("ChainEitherK with error", func(t *testing.T) {
|
||||
@@ -294,7 +294,7 @@ func TestMonadChainEitherK(t *testing.T) {
|
||||
result := MonadChainEitherK(Of(5), func(x int) Either[int] {
|
||||
return E.Left[int](err)
|
||||
})
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -303,7 +303,7 @@ func TestChainEitherK(t *testing.T) {
|
||||
return E.Right[error](x * 2)
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadChainFirstEitherK(t *testing.T) {
|
||||
@@ -311,7 +311,7 @@ func TestMonadChainFirstEitherK(t *testing.T) {
|
||||
result := MonadChainFirstEitherK(Of(5), func(x int) Either[string] {
|
||||
return E.Right[error]("ignored")
|
||||
})
|
||||
assert.Equal(t, E.Right[error](5), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](5), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("ChainFirstEitherK propagates error", func(t *testing.T) {
|
||||
@@ -319,7 +319,7 @@ func TestMonadChainFirstEitherK(t *testing.T) {
|
||||
result := MonadChainFirstEitherK(Of(5), func(x int) Either[string] {
|
||||
return E.Left[string](err)
|
||||
})
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -328,7 +328,7 @@ func TestChainFirstEitherK(t *testing.T) {
|
||||
return E.Right[error]("ignored")
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
assert.Equal(t, E.Right[error](5), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](5), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestChainOptionK(t *testing.T) {
|
||||
@@ -339,7 +339,7 @@ func TestChainOptionK(t *testing.T) {
|
||||
return O.Some(x * 2)
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("ChainOptionK with None", func(t *testing.T) {
|
||||
@@ -349,7 +349,7 @@ func TestChainOptionK(t *testing.T) {
|
||||
return O.None[int]()
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsLeft(res))
|
||||
})
|
||||
}
|
||||
@@ -358,44 +358,44 @@ func TestFromIOEither(t *testing.T) {
|
||||
t.Run("FromIOEither with success", func(t *testing.T) {
|
||||
ioe := IOE.Of[error](42)
|
||||
result := FromIOEither(ioe)
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("FromIOEither with error", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
ioe := IOE.Left[int](err)
|
||||
result := FromIOEither(ioe)
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIOResult(t *testing.T) {
|
||||
ioe := IOE.Of[error](42)
|
||||
result := FromIOResult(ioe)
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestFromIO(t *testing.T) {
|
||||
io := IOG.Of(42)
|
||||
result := FromIO(io)
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestFromReader(t *testing.T) {
|
||||
reader := R.Of[context.Context](42)
|
||||
result := FromReader(reader)
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestFromLazy(t *testing.T) {
|
||||
lazy := func() int { return 42 }
|
||||
result := FromLazy(lazy)
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestNever(t *testing.T) {
|
||||
t.Run("Never with cancelled context", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
result := Never[int]()
|
||||
|
||||
// Cancel immediately
|
||||
@@ -406,7 +406,7 @@ func TestNever(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Never with timeout", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
result := Never[int]()
|
||||
@@ -419,7 +419,7 @@ func TestMonadChainIOK(t *testing.T) {
|
||||
result := MonadChainIOK(Of(5), func(x int) IOG.IO[int] {
|
||||
return IOG.Of(x * 2)
|
||||
})
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestChainIOK(t *testing.T) {
|
||||
@@ -427,14 +427,14 @@ func TestChainIOK(t *testing.T) {
|
||||
return IOG.Of(x * 2)
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadChainFirstIOK(t *testing.T) {
|
||||
result := MonadChainFirstIOK(Of(5), func(x int) IOG.IO[string] {
|
||||
return IOG.Of("ignored")
|
||||
})
|
||||
assert.Equal(t, E.Right[error](5), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](5), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestChainFirstIOK(t *testing.T) {
|
||||
@@ -442,7 +442,7 @@ func TestChainFirstIOK(t *testing.T) {
|
||||
return IOG.Of("ignored")
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
assert.Equal(t, E.Right[error](5), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](5), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestChainIOEitherK(t *testing.T) {
|
||||
@@ -451,7 +451,7 @@ func TestChainIOEitherK(t *testing.T) {
|
||||
return IOE.Of[error](x * 2)
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("ChainIOEitherK with error", func(t *testing.T) {
|
||||
@@ -460,7 +460,7 @@ func TestChainIOEitherK(t *testing.T) {
|
||||
return IOE.Left[int](err)
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -469,7 +469,7 @@ func TestDelay(t *testing.T) {
|
||||
start := time.Now()
|
||||
delayed := Delay[int](100 * time.Millisecond)
|
||||
result := delayed(Of(42))
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
elapsed := time.Since(start)
|
||||
|
||||
assert.True(t, E.IsRight(res))
|
||||
@@ -477,7 +477,7 @@ func TestDelay(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Delay with cancelled context", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
delayed := Delay[int](100 * time.Millisecond)
|
||||
result := delayed(Of(42))
|
||||
@@ -500,11 +500,11 @@ func TestDefer(t *testing.T) {
|
||||
})
|
||||
|
||||
// First execution
|
||||
res1 := deferred(context.Background())()
|
||||
res1 := deferred(t.Context())()
|
||||
assert.True(t, E.IsRight(res1))
|
||||
|
||||
// Second execution should generate a new computation
|
||||
res2 := deferred(context.Background())()
|
||||
res2 := deferred(t.Context())()
|
||||
assert.True(t, E.IsRight(res2))
|
||||
|
||||
// Counter should be incremented for each execution
|
||||
@@ -518,7 +518,7 @@ func TestTryCatch(t *testing.T) {
|
||||
return 42, nil
|
||||
}
|
||||
})
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("TryCatch with error", func(t *testing.T) {
|
||||
@@ -528,7 +528,7 @@ func TestTryCatch(t *testing.T) {
|
||||
return 0, err
|
||||
}
|
||||
})
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -537,7 +537,7 @@ func TestMonadAlt(t *testing.T) {
|
||||
first := Of(42)
|
||||
second := func() ReaderIOResult[int] { return Of(100) }
|
||||
result := MonadAlt(first, second)
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("Alt with first error", func(t *testing.T) {
|
||||
@@ -545,7 +545,7 @@ func TestMonadAlt(t *testing.T) {
|
||||
first := Left[int](err)
|
||||
second := func() ReaderIOResult[int] { return Of(100) }
|
||||
result := MonadAlt(first, second)
|
||||
assert.Equal(t, E.Right[error](100), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](100), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -553,7 +553,7 @@ func TestAlt(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
alternative := Alt(func() ReaderIOResult[int] { return Of(100) })
|
||||
result := alternative(Left[int](err))
|
||||
assert.Equal(t, E.Right[error](100), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](100), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMemoize(t *testing.T) {
|
||||
@@ -564,13 +564,13 @@ func TestMemoize(t *testing.T) {
|
||||
}))
|
||||
|
||||
// First execution
|
||||
res1 := computation(context.Background())()
|
||||
res1 := computation(t.Context())()
|
||||
assert.True(t, E.IsRight(res1))
|
||||
val1 := E.ToOption(res1)
|
||||
assert.Equal(t, O.Of(1), val1)
|
||||
|
||||
// Second execution should return cached value
|
||||
res2 := computation(context.Background())()
|
||||
res2 := computation(t.Context())()
|
||||
assert.True(t, E.IsRight(res2))
|
||||
val2 := E.ToOption(res2)
|
||||
assert.Equal(t, O.Of(1), val2)
|
||||
@@ -582,19 +582,19 @@ func TestMemoize(t *testing.T) {
|
||||
func TestFlatten(t *testing.T) {
|
||||
nested := Of(Of(42))
|
||||
result := Flatten(nested)
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadFlap(t *testing.T) {
|
||||
fab := Of(N.Mul(2))
|
||||
result := MonadFlap(fab, 5)
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestFlap(t *testing.T) {
|
||||
flapper := Flap[int](5)
|
||||
result := flapper(Of(N.Mul(2)))
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestFold(t *testing.T) {
|
||||
@@ -608,7 +608,7 @@ func TestFold(t *testing.T) {
|
||||
},
|
||||
)
|
||||
result := folder(Of(42))
|
||||
assert.Equal(t, E.Right[error]("success: 42"), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error]("success: 42"), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("Fold with error", func(t *testing.T) {
|
||||
@@ -622,7 +622,7 @@ func TestFold(t *testing.T) {
|
||||
},
|
||||
)
|
||||
result := folder(Left[int](err))
|
||||
assert.Equal(t, E.Right[error]("error: test error"), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error]("error: test error"), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -634,7 +634,7 @@ func TestGetOrElse(t *testing.T) {
|
||||
}
|
||||
})
|
||||
result := getter(Of(42))
|
||||
assert.Equal(t, 42, result(context.Background())())
|
||||
assert.Equal(t, 42, result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("GetOrElse with error", func(t *testing.T) {
|
||||
@@ -645,19 +645,19 @@ func TestGetOrElse(t *testing.T) {
|
||||
}
|
||||
})
|
||||
result := getter(Left[int](err))
|
||||
assert.Equal(t, 0, result(context.Background())())
|
||||
assert.Equal(t, 0, result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithContext(t *testing.T) {
|
||||
t.Run("WithContext with valid context", func(t *testing.T) {
|
||||
computation := WithContext(Of(42))
|
||||
result := computation(context.Background())()
|
||||
result := computation(t.Context())()
|
||||
assert.Equal(t, E.Right[error](42), result)
|
||||
})
|
||||
|
||||
t.Run("WithContext with cancelled context", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
computation := WithContext(Of(42))
|
||||
@@ -672,7 +672,7 @@ func TestEitherize0(t *testing.T) {
|
||||
}
|
||||
eitherized := Eitherize0(f)
|
||||
result := eitherized()
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestUneitherize0(t *testing.T) {
|
||||
@@ -680,7 +680,7 @@ func TestUneitherize0(t *testing.T) {
|
||||
return Of(42)
|
||||
}
|
||||
uneitherized := Uneitherize0(f)
|
||||
result, err := uneitherized(context.Background())
|
||||
result, err := uneitherized(t.Context())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
@@ -691,7 +691,7 @@ func TestEitherize1(t *testing.T) {
|
||||
}
|
||||
eitherized := Eitherize1(f)
|
||||
result := eitherized(5)
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestUneitherize1(t *testing.T) {
|
||||
@@ -699,14 +699,14 @@ func TestUneitherize1(t *testing.T) {
|
||||
return Of(x * 2)
|
||||
}
|
||||
uneitherized := Uneitherize1(f)
|
||||
result, err := uneitherized(context.Background(), 5)
|
||||
result, err := uneitherized(t.Context(), 5)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 10, result)
|
||||
}
|
||||
|
||||
func TestSequenceT2(t *testing.T) {
|
||||
result := SequenceT2(Of(1), Of(2))
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsRight(res))
|
||||
tuple := E.ToOption(res)
|
||||
assert.True(t, O.IsSome(tuple))
|
||||
@@ -717,13 +717,13 @@ func TestSequenceT2(t *testing.T) {
|
||||
|
||||
func TestSequenceSeqT2(t *testing.T) {
|
||||
result := SequenceSeqT2(Of(1), Of(2))
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsRight(res))
|
||||
}
|
||||
|
||||
func TestSequenceParT2(t *testing.T) {
|
||||
result := SequenceParT2(Of(1), Of(2))
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsRight(res))
|
||||
}
|
||||
|
||||
@@ -734,7 +734,7 @@ func TestTraverseArray(t *testing.T) {
|
||||
return Of(x * 2)
|
||||
})
|
||||
result := traverser(arr)
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsRight(res))
|
||||
arrOpt := E.ToOption(res)
|
||||
assert.Equal(t, O.Of([]int{2, 4, 6}), arrOpt)
|
||||
@@ -750,7 +750,7 @@ func TestTraverseArray(t *testing.T) {
|
||||
return Of(x * 2)
|
||||
})
|
||||
result := traverser(arr)
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsLeft(res))
|
||||
})
|
||||
}
|
||||
@@ -758,7 +758,7 @@ func TestTraverseArray(t *testing.T) {
|
||||
func TestSequenceArray(t *testing.T) {
|
||||
arr := []ReaderIOResult[int]{Of(1), Of(2), Of(3)}
|
||||
result := SequenceArray(arr)
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsRight(res))
|
||||
arrOpt := E.ToOption(res)
|
||||
assert.Equal(t, O.Of([]int{1, 2, 3}), arrOpt)
|
||||
@@ -769,7 +769,7 @@ func TestTraverseRecord(t *testing.T) {
|
||||
result := TraverseRecord[string](func(x int) ReaderIOResult[int] {
|
||||
return Of(x * 2)
|
||||
})(rec)
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsRight(res))
|
||||
recOpt := E.ToOption(res)
|
||||
assert.True(t, O.IsSome(recOpt))
|
||||
@@ -784,7 +784,7 @@ func TestSequenceRecord(t *testing.T) {
|
||||
"b": Of(2),
|
||||
}
|
||||
result := SequenceRecord(rec)
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsRight(res))
|
||||
recOpt := E.ToOption(res)
|
||||
assert.True(t, O.IsSome(recOpt))
|
||||
@@ -798,7 +798,7 @@ func TestAltSemigroup(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
|
||||
result := sg.Concat(Left[int](err), Of(42))
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.Equal(t, E.Right[error](42), res)
|
||||
}
|
||||
|
||||
@@ -810,7 +810,7 @@ func TestApplicativeMonoid(t *testing.T) {
|
||||
))
|
||||
|
||||
result := intAddMonoid.Concat(Of(5), Of(10))
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.Equal(t, E.Right[error](15), res)
|
||||
}
|
||||
|
||||
@@ -835,7 +835,7 @@ func TestBracket(t *testing.T) {
|
||||
}
|
||||
|
||||
result := Bracket(acquire, use, release)
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
|
||||
assert.True(t, acquired)
|
||||
assert.True(t, released)
|
||||
@@ -863,7 +863,7 @@ func TestBracket(t *testing.T) {
|
||||
}
|
||||
|
||||
result := Bracket(acquire, use, release)
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
|
||||
assert.True(t, acquired)
|
||||
assert.True(t, released)
|
||||
|
||||
@@ -29,7 +29,7 @@ import (
|
||||
|
||||
func TestInnerContextCancelSemantics(t *testing.T) {
|
||||
// start with a simple context
|
||||
outer := context.Background()
|
||||
outer := t.Context()
|
||||
|
||||
parent, parentCancel := context.WithCancel(outer)
|
||||
defer parentCancel()
|
||||
@@ -49,7 +49,7 @@ func TestInnerContextCancelSemantics(t *testing.T) {
|
||||
|
||||
func TestOuterContextCancelSemantics(t *testing.T) {
|
||||
// start with a simple context
|
||||
outer := context.Background()
|
||||
outer := t.Context()
|
||||
|
||||
parent, outerCancel := context.WithCancel(outer)
|
||||
defer outerCancel()
|
||||
@@ -69,7 +69,7 @@ func TestOuterContextCancelSemantics(t *testing.T) {
|
||||
|
||||
func TestOuterAndInnerContextCancelSemantics(t *testing.T) {
|
||||
// start with a simple context
|
||||
outer := context.Background()
|
||||
outer := t.Context()
|
||||
|
||||
parent, outerCancel := context.WithCancel(outer)
|
||||
defer outerCancel()
|
||||
@@ -95,7 +95,7 @@ func TestOuterAndInnerContextCancelSemantics(t *testing.T) {
|
||||
|
||||
func TestCancelCauseSemantics(t *testing.T) {
|
||||
// start with a simple context
|
||||
outer := context.Background()
|
||||
outer := t.Context()
|
||||
|
||||
parent, outerCancel := context.WithCancelCause(outer)
|
||||
defer outerCancel(nil)
|
||||
@@ -119,7 +119,7 @@ func TestCancelCauseSemantics(t *testing.T) {
|
||||
func TestTimer(t *testing.T) {
|
||||
delta := 3 * time.Second
|
||||
timer := Timer(delta)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
t0 := time.Now()
|
||||
res := timer(ctx)()
|
||||
@@ -146,7 +146,7 @@ func TestCanceledApply(t *testing.T) {
|
||||
Ap[string](errValue),
|
||||
)
|
||||
|
||||
res := applied(context.Background())()
|
||||
res := applied(t.Context())()
|
||||
assert.Equal(t, E.Left[string](err), res)
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ func TestRegularApply(t *testing.T) {
|
||||
Ap[string](value),
|
||||
)
|
||||
|
||||
res := applied(context.Background())()
|
||||
res := applied(t.Context())()
|
||||
assert.Equal(t, E.Of[error]("CARSTEN"), res)
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ func TestWithResourceNoErrors(t *testing.T) {
|
||||
|
||||
resRIOE := WithResource[int](acquire, release)(body)
|
||||
|
||||
res := resRIOE(context.Background())()
|
||||
res := resRIOE(t.Context())()
|
||||
|
||||
assert.Equal(t, 1, countAcquire)
|
||||
assert.Equal(t, 1, countBody)
|
||||
@@ -217,7 +217,7 @@ func TestWithResourceErrorInBody(t *testing.T) {
|
||||
|
||||
resRIOE := WithResource[int](acquire, release)(body)
|
||||
|
||||
res := resRIOE(context.Background())()
|
||||
res := resRIOE(t.Context())()
|
||||
|
||||
assert.Equal(t, 1, countAcquire)
|
||||
assert.Equal(t, 0, countBody)
|
||||
@@ -247,7 +247,7 @@ func TestWithResourceErrorInAcquire(t *testing.T) {
|
||||
|
||||
resRIOE := WithResource[int](acquire, release)(body)
|
||||
|
||||
res := resRIOE(context.Background())()
|
||||
res := resRIOE(t.Context())()
|
||||
|
||||
assert.Equal(t, 0, countAcquire)
|
||||
assert.Equal(t, 0, countBody)
|
||||
@@ -277,7 +277,7 @@ func TestWithResourceErrorInRelease(t *testing.T) {
|
||||
|
||||
resRIOE := WithResource[int](acquire, release)(body)
|
||||
|
||||
res := resRIOE(context.Background())()
|
||||
res := resRIOE(t.Context())()
|
||||
|
||||
assert.Equal(t, 1, countAcquire)
|
||||
assert.Equal(t, 1, countBody)
|
||||
@@ -286,7 +286,7 @@ func TestWithResourceErrorInRelease(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadChainFirstLeft(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with Left value - function returns Left, always preserves original error
|
||||
t.Run("Left value with function returning Left preserves original error", func(t *testing.T) {
|
||||
@@ -353,7 +353,7 @@ func TestMonadChainFirstLeft(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestChainFirstLeft(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with Left value - function returns Left, always preserves original error
|
||||
t.Run("Left value with function returning Left preserves error", func(t *testing.T) {
|
||||
|
||||
@@ -108,7 +108,7 @@ import (
|
||||
// countdown := readerioresult.TailRec(countdownStep)
|
||||
//
|
||||
// // With cancellation
|
||||
// ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
|
||||
// ctx, cancel := context.WithTimeout(t.Context(), 500*time.Millisecond)
|
||||
// defer cancel()
|
||||
// result := countdown(10)(ctx)() // Will be cancelled after ~500ms
|
||||
//
|
||||
@@ -141,7 +141,7 @@ import (
|
||||
// }
|
||||
//
|
||||
// processFiles := readerioresult.TailRec(processStep)
|
||||
// ctx, cancel := context.WithCancel(context.Background())
|
||||
// ctx, cancel := context.WithCancel(t.Context())
|
||||
//
|
||||
// // Can be cancelled at any point during processing
|
||||
// go func() {
|
||||
@@ -159,7 +159,7 @@ import (
|
||||
//
|
||||
// // Safe for very large inputs with cancellation support
|
||||
// largeCountdown := readerioresult.TailRec(countdownStep)
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// result := largeCountdown(1000000)(ctx)() // Safe, no stack overflow
|
||||
//
|
||||
// # Performance Considerations
|
||||
|
||||
@@ -44,7 +44,7 @@ func TestTailRec_BasicRecursion(t *testing.T) {
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(5)(context.Background())()
|
||||
result := countdown(5)(t.Context())()
|
||||
|
||||
assert.Equal(t, E.Of[error]("Done!"), result)
|
||||
}
|
||||
@@ -71,7 +71,7 @@ func TestTailRec_FactorialRecursion(t *testing.T) {
|
||||
}
|
||||
|
||||
factorial := TailRec(factorialStep)
|
||||
result := factorial(FactorialState{n: 5, acc: 1})(context.Background())()
|
||||
result := factorial(FactorialState{n: 5, acc: 1})(t.Context())()
|
||||
|
||||
assert.Equal(t, E.Of[error](120), result) // 5! = 120
|
||||
}
|
||||
@@ -95,7 +95,7 @@ func TestTailRec_ErrorHandling(t *testing.T) {
|
||||
}
|
||||
|
||||
errorRecursion := TailRec(errorStep)
|
||||
result := errorRecursion(5)(context.Background())()
|
||||
result := errorRecursion(5)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
err := E.ToError(result)
|
||||
@@ -125,7 +125,7 @@ func TestTailRec_ContextCancellation(t *testing.T) {
|
||||
slowRecursion := TailRec(slowStep)
|
||||
|
||||
// Create a context that will be cancelled after 100ms
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
start := time.Now()
|
||||
@@ -159,7 +159,7 @@ func TestTailRec_ImmediateCancellation(t *testing.T) {
|
||||
countdown := TailRec(countdownStep)
|
||||
|
||||
// Create an already cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
result := countdown(5)(ctx)()
|
||||
@@ -186,7 +186,7 @@ func TestTailRec_StackSafety(t *testing.T) {
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(largeN)(context.Background())()
|
||||
result := countdown(largeN)(t.Context())()
|
||||
|
||||
assert.Equal(t, E.Of[error](0), result)
|
||||
}
|
||||
@@ -217,7 +217,7 @@ func TestTailRec_StackSafetyWithCancellation(t *testing.T) {
|
||||
countdown := TailRec(countdownStep)
|
||||
|
||||
// Cancel after 50ms to allow some iterations but not all
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
result := countdown(largeN)(ctx)()
|
||||
@@ -274,7 +274,7 @@ func TestTailRec_ComplexState(t *testing.T) {
|
||||
errors: []error{},
|
||||
}
|
||||
|
||||
result := processItems(initialState)(context.Background())()
|
||||
result := processItems(initialState)(t.Context())()
|
||||
|
||||
assert.Equal(t, E.Of[error]([]string{"item1", "item2", "item3"}), result)
|
||||
})
|
||||
@@ -286,7 +286,7 @@ func TestTailRec_ComplexState(t *testing.T) {
|
||||
errors: []error{},
|
||||
}
|
||||
|
||||
result := processItems(initialState)(context.Background())()
|
||||
result := processItems(initialState)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
err := E.ToError(result)
|
||||
@@ -336,7 +336,7 @@ func TestTailRec_CancellationDuringProcessing(t *testing.T) {
|
||||
}
|
||||
|
||||
// Cancel after 100ms (should allow ~5 files to be processed)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
start := time.Now()
|
||||
@@ -366,7 +366,7 @@ func TestTailRec_ZeroIterations(t *testing.T) {
|
||||
}
|
||||
|
||||
immediate := TailRec(immediateStep)
|
||||
result := immediate(100)(context.Background())()
|
||||
result := immediate(100)(t.Context())()
|
||||
|
||||
assert.Equal(t, E.Of[error]("immediate"), result)
|
||||
}
|
||||
@@ -392,7 +392,7 @@ func TestTailRec_ContextWithDeadline(t *testing.T) {
|
||||
slowRecursion := TailRec(slowStep)
|
||||
|
||||
// Set deadline 80ms from now
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(80*time.Millisecond))
|
||||
ctx, cancel := context.WithDeadline(t.Context(), time.Now().Add(80*time.Millisecond))
|
||||
defer cancel()
|
||||
|
||||
result := slowRecursion(10)(ctx)()
|
||||
@@ -427,7 +427,7 @@ func TestTailRec_ContextWithValue(t *testing.T) {
|
||||
}
|
||||
|
||||
valueRecursion := TailRec(valueStep)
|
||||
ctx := context.WithValue(context.Background(), testKey, "test-value")
|
||||
ctx := context.WithValue(t.Context(), testKey, "test-value")
|
||||
result := valueRecursion(3)(ctx)()
|
||||
|
||||
assert.Equal(t, E.Of[error]("Done!"), result)
|
||||
|
||||
@@ -107,7 +107,7 @@ import (
|
||||
// retryingFetch := Retrying(policy, fetchData, shouldRetry)
|
||||
//
|
||||
// // Execute with a cancellable context
|
||||
// ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
// ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||
// defer cancel()
|
||||
// ioResult := retryingFetch(ctx)
|
||||
// finalResult := ioResult()
|
||||
|
||||
@@ -113,7 +113,7 @@ type (
|
||||
// }
|
||||
//
|
||||
// The computation is executed by providing a context and then invoking the result:
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// result := fetchUser("123")(ctx)()
|
||||
ReaderIOResult[A any] = RIOR.ReaderIOResult[context.Context, A]
|
||||
|
||||
|
||||
453
v2/context/readerreaderioresult/bind.go
Normal file
453
v2/context/readerreaderioresult/bind.go
Normal file
@@ -0,0 +1,453 @@
|
||||
// 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 readerreaderioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/apply"
|
||||
"github.com/IBM/fp-go/v2/internal/chain"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"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"
|
||||
)
|
||||
|
||||
// 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 with two reader contexts.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Posts []Post
|
||||
// }
|
||||
// type OuterEnv struct {
|
||||
// Database string
|
||||
// }
|
||||
// type InnerEnv struct {
|
||||
// UserRepo UserRepository
|
||||
// PostRepo PostRepository
|
||||
// }
|
||||
// result := readerreaderioeither.Do[OuterEnv, InnerEnv, error](State{})
|
||||
//
|
||||
//go:inline
|
||||
func Do[R, S any](
|
||||
empty S,
|
||||
) ReaderReaderIOResult[R, S] {
|
||||
return Of[R](empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// This enables sequential composition where each step can depend on the results of previous steps
|
||||
// and access both the outer (R) and inner (C) reader environments.
|
||||
//
|
||||
// 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
|
||||
// Posts []Post
|
||||
// }
|
||||
// type OuterEnv struct {
|
||||
// Database string
|
||||
// }
|
||||
// type InnerEnv struct {
|
||||
// UserRepo UserRepository
|
||||
// PostRepo PostRepository
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readerreaderioeither.Do[OuterEnv, InnerEnv, error](State{}),
|
||||
// readerreaderioeither.Bind(
|
||||
// func(user User) func(State) State {
|
||||
// return func(s State) State { s.User = user; return s }
|
||||
// },
|
||||
// func(s State) readerreaderioeither.ReaderReaderIOResult[OuterEnv, InnerEnv, error, User] {
|
||||
// return func(outer OuterEnv) readerioeither.ReaderIOEither[InnerEnv, error, User] {
|
||||
// return readerioeither.Asks(func(inner InnerEnv) ioeither.IOEither[error, User] {
|
||||
// return inner.UserRepo.FindUser(outer.Database)
|
||||
// })
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// readerreaderioeither.Bind(
|
||||
// func(posts []Post) func(State) State {
|
||||
// return func(s State) State { s.Posts = posts; return s }
|
||||
// },
|
||||
// func(s State) readerreaderioeither.ReaderReaderIOResult[OuterEnv, InnerEnv, error, []Post] {
|
||||
// return func(outer OuterEnv) readerioeither.ReaderIOEither[InnerEnv, error, []Post] {
|
||||
// return readerioeither.Asks(func(inner InnerEnv) ioeither.IOEither[error, []Post] {
|
||||
// return inner.PostRepo.FindPostsByUser(outer.Database, s.User.ID)
|
||||
// })
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func Bind[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) ReaderReaderIOResult[R, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return chain.Bind(
|
||||
Chain[R, S1, S2],
|
||||
Map[R, T, S2],
|
||||
setter,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// Let attaches a pure computation result to a context [S1] to produce a context [S2].
|
||||
// Unlike [Bind], the computation function f is pure (doesn't perform effects).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// readerreaderioeither.Let(
|
||||
// func(fullName string) func(State) State {
|
||||
// return func(s State) State { s.FullName = fullName; return s }
|
||||
// },
|
||||
// func(s State) string {
|
||||
// return s.FirstName + " " + s.LastName
|
||||
// },
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func Let[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
) Operator[R, S1, S2] {
|
||||
return functor.Let(
|
||||
Map[R, S1, S2],
|
||||
setter,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// LetTo attaches a constant value to a context [S1] to produce a context [S2].
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// readerreaderioeither.LetTo(
|
||||
// func(status string) func(State) State {
|
||||
// return func(s State) State { s.Status = status; return s }
|
||||
// },
|
||||
// "active",
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func LetTo[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
) Operator[R, S1, S2] {
|
||||
return functor.LetTo(
|
||||
Map[R, S1, S2],
|
||||
setter,
|
||||
b,
|
||||
)
|
||||
}
|
||||
|
||||
// BindTo wraps a value of type T into a context S1 using the provided setter function.
|
||||
// This is typically used as the first operation after [Do] to initialize the context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// F.Pipe1(
|
||||
// readerreaderioeither.Of[OuterEnv, InnerEnv, error](42),
|
||||
// readerreaderioeither.BindTo(func(n int) State { return State{Count: n} }),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func BindTo[R, S1, T any](
|
||||
setter func(T) S1,
|
||||
) Operator[R, T, S1] {
|
||||
return chain.BindTo(
|
||||
Map[R, T, S1],
|
||||
setter,
|
||||
)
|
||||
}
|
||||
|
||||
// ApS applies a computation in parallel (applicative style) and attaches its result to the context.
|
||||
// Unlike [Bind], this doesn't allow the computation to depend on the current context state.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// readerreaderioeither.ApS(
|
||||
// func(count int) func(State) State {
|
||||
// return func(s State) State { s.Count = count; return s }
|
||||
// },
|
||||
// getCount, // ReaderReaderIOResult[OuterEnv, InnerEnv, error, int]
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApS[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa ReaderReaderIOResult[R, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return apply.ApS(
|
||||
Ap[S2, R, T],
|
||||
Map[R, S1, func(T) S2],
|
||||
setter,
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// ApSL is a lens-based version of [ApS] that uses a lens to focus on a specific field in the context.
|
||||
//
|
||||
//go:inline
|
||||
func ApSL[R, S, T any](
|
||||
lens Lens[S, T],
|
||||
fa ReaderReaderIOResult[R, T],
|
||||
) Operator[R, S, S] {
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// BindL is a lens-based version of [Bind] that uses a lens to focus on a specific field in the context.
|
||||
//
|
||||
//go:inline
|
||||
func BindL[R, S, T any](
|
||||
lens Lens[S, T],
|
||||
f func(T) ReaderReaderIOResult[R, T],
|
||||
) Operator[R, S, S] {
|
||||
return Bind(lens.Set, F.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetL is a lens-based version of [Let] that uses a lens to focus on a specific field in the context.
|
||||
//
|
||||
//go:inline
|
||||
func LetL[R, S, T any](
|
||||
lens Lens[S, T],
|
||||
f func(T) T,
|
||||
) Operator[R, S, S] {
|
||||
return Let[R](lens.Set, F.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetToL is a lens-based version of [LetTo] that uses a lens to focus on a specific field in the context.
|
||||
//
|
||||
//go:inline
|
||||
func LetToL[R, S, T any](
|
||||
lens Lens[S, T],
|
||||
b T,
|
||||
) Operator[R, S, S] {
|
||||
return LetTo[R](lens.Set, b)
|
||||
}
|
||||
|
||||
// BindIOEitherK binds a computation that returns an IOEither to the context.
|
||||
// The Kleisli function is automatically lifted into ReaderReaderIOResult.
|
||||
//
|
||||
//go:inline
|
||||
func BindIOEitherK[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f ioeither.Kleisli[error, S1, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return Bind(setter, F.Flow2(f, FromIOEither[R, T]))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindIOResultK[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f ioresult.Kleisli[S1, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return Bind(setter, F.Flow2(f, FromIOResult[R, T]))
|
||||
}
|
||||
|
||||
// BindIOK binds a computation that returns an IO to the context.
|
||||
// The Kleisli function is automatically lifted into ReaderReaderIOResult.
|
||||
//
|
||||
//go:inline
|
||||
func BindIOK[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f io.Kleisli[S1, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return Bind(setter, F.Flow2(f, FromIO[R, T]))
|
||||
}
|
||||
|
||||
// BindReaderK binds a computation that returns a Reader to the context.
|
||||
// The Kleisli function is automatically lifted into ReaderReaderIOResult.
|
||||
//
|
||||
//go:inline
|
||||
func BindReaderK[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f reader.Kleisli[R, S1, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return Bind(setter, F.Flow2(f, FromReader[R, T]))
|
||||
}
|
||||
|
||||
// BindReaderIOK binds a computation that returns a ReaderIO to the context.
|
||||
// The Kleisli function is automatically lifted into ReaderReaderIOResult.
|
||||
//
|
||||
//go:inline
|
||||
func BindReaderIOK[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f readerio.Kleisli[R, S1, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return Bind(setter, F.Flow2(f, FromReaderIO[R, T]))
|
||||
}
|
||||
|
||||
// BindEitherK binds a computation that returns an Either to the context.
|
||||
// The Kleisli function is automatically lifted into ReaderReaderIOResult.
|
||||
//
|
||||
//go:inline
|
||||
func BindEitherK[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f either.Kleisli[error, S1, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return Bind(setter, F.Flow2(f, FromEither[R, T]))
|
||||
}
|
||||
|
||||
// BindIOEitherKL is a lens-based version of [BindIOEitherK].
|
||||
//
|
||||
//go:inline
|
||||
func BindIOEitherKL[R, S, T any](
|
||||
lens Lens[S, T],
|
||||
f ioeither.Kleisli[error, T, T],
|
||||
) Operator[R, S, S] {
|
||||
return BindL(lens, F.Flow2(f, FromIOEither[R, T]))
|
||||
}
|
||||
|
||||
// BindIOKL is a lens-based version of [BindIOK].
|
||||
//
|
||||
//go:inline
|
||||
func BindIOKL[R, S, T any](
|
||||
lens Lens[S, T],
|
||||
f io.Kleisli[T, T],
|
||||
) Operator[R, S, S] {
|
||||
return BindL(lens, F.Flow2(f, FromIO[R, T]))
|
||||
}
|
||||
|
||||
// BindReaderKL is a lens-based version of [BindReaderK].
|
||||
//
|
||||
//go:inline
|
||||
func BindReaderKL[R, S, T any](
|
||||
lens Lens[S, T],
|
||||
f reader.Kleisli[R, T, T],
|
||||
) Operator[R, S, S] {
|
||||
return BindL(lens, F.Flow2(f, FromReader[R, T]))
|
||||
}
|
||||
|
||||
// BindReaderIOKL is a lens-based version of [BindReaderIOK].
|
||||
//
|
||||
//go:inline
|
||||
func BindReaderIOKL[R, S, T any](
|
||||
lens Lens[S, T],
|
||||
f readerio.Kleisli[R, T, T],
|
||||
) Operator[R, S, S] {
|
||||
return BindL(lens, F.Flow2(f, FromReaderIO[R, T]))
|
||||
}
|
||||
|
||||
// ApIOEitherS applies an IOEither computation and attaches its result to the context.
|
||||
//
|
||||
//go:inline
|
||||
func ApIOEitherS[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa IOEither[error, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return ApS(setter, FromIOEither[R](fa))
|
||||
}
|
||||
|
||||
// ApIOS applies an IO computation and attaches its result to the context.
|
||||
//
|
||||
//go:inline
|
||||
func ApIOS[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa IO[T],
|
||||
) Operator[R, S1, S2] {
|
||||
return ApS(setter, FromIO[R](fa))
|
||||
}
|
||||
|
||||
// ApReaderS applies a Reader computation and attaches its result to the context.
|
||||
//
|
||||
//go:inline
|
||||
func ApReaderS[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Reader[R, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return ApS(setter, FromReader(fa))
|
||||
}
|
||||
|
||||
// ApReaderIOS applies a ReaderIO computation and attaches its result to the context.
|
||||
//
|
||||
//go:inline
|
||||
func ApReaderIOS[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa ReaderIO[R, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return ApS(setter, FromReaderIO(fa))
|
||||
}
|
||||
|
||||
// ApEitherS applies an Either computation and attaches its result to the context.
|
||||
//
|
||||
//go:inline
|
||||
func ApEitherS[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Either[error, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return ApS(setter, FromEither[R](fa))
|
||||
}
|
||||
|
||||
// ApIOEitherSL is a lens-based version of [ApIOEitherS].
|
||||
//
|
||||
//go:inline
|
||||
func ApIOEitherSL[R, S, T any](
|
||||
lens Lens[S, T],
|
||||
fa IOEither[error, T],
|
||||
) Operator[R, S, S] {
|
||||
return ApIOEitherS[R](lens.Set, fa)
|
||||
}
|
||||
|
||||
// ApIOSL is a lens-based version of [ApIOS].
|
||||
//
|
||||
//go:inline
|
||||
func ApIOSL[R, S, T any](
|
||||
lens Lens[S, T],
|
||||
fa IO[T],
|
||||
) Operator[R, S, S] {
|
||||
return ApSL(lens, FromIO[R](fa))
|
||||
}
|
||||
|
||||
// ApReaderSL is a lens-based version of [ApReaderS].
|
||||
//
|
||||
//go:inline
|
||||
func ApReaderSL[R, S, T any](
|
||||
lens Lens[S, T],
|
||||
fa Reader[R, T],
|
||||
) Operator[R, S, S] {
|
||||
return ApReaderS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// ApReaderIOSL is a lens-based version of [ApReaderIOS].
|
||||
//
|
||||
//go:inline
|
||||
func ApReaderIOSL[R, S, T any](
|
||||
lens Lens[S, T],
|
||||
fa ReaderIO[R, T],
|
||||
) Operator[R, S, S] {
|
||||
return ApReaderIOS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// ApEitherSL is a lens-based version of [ApEitherS].
|
||||
//
|
||||
//go:inline
|
||||
func ApEitherSL[R, S, T any](
|
||||
lens Lens[S, T],
|
||||
fa Either[error, T],
|
||||
) Operator[R, S, S] {
|
||||
return ApEitherS[R](lens.Set, fa)
|
||||
}
|
||||
720
v2/context/readerreaderioresult/bind_test.go
Normal file
720
v2/context/readerreaderioresult/bind_test.go
Normal file
@@ -0,0 +1,720 @@
|
||||
// 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 readerreaderioresult
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type AppConfig struct {
|
||||
DatabaseURL string
|
||||
LogLevel string
|
||||
}
|
||||
|
||||
var defaultConfig = AppConfig{
|
||||
DatabaseURL: "postgres://localhost",
|
||||
LogLevel: "info",
|
||||
}
|
||||
|
||||
func getLastName(s utils.Initial) ReaderReaderIOResult[AppConfig, string] {
|
||||
return Of[AppConfig]("Doe")
|
||||
}
|
||||
|
||||
func getGivenName(s utils.WithLastName) ReaderReaderIOResult[AppConfig, string] {
|
||||
return Of[AppConfig]("John")
|
||||
}
|
||||
|
||||
func TestDo(t *testing.T) {
|
||||
res := Do[AppConfig](utils.Empty)
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
|
||||
assert.True(t, result.IsRight(outcome))
|
||||
assert.Equal(t, result.Of(utils.Empty), outcome)
|
||||
}
|
||||
|
||||
func TestBind(t *testing.T) {
|
||||
res := F.Pipe3(
|
||||
Do[AppConfig](utils.Empty),
|
||||
Bind(utils.SetLastName, getLastName),
|
||||
Bind(utils.SetGivenName, getGivenName),
|
||||
Map[AppConfig](utils.GetFullName),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of("John Doe"), outcome)
|
||||
}
|
||||
|
||||
func TestBindWithError(t *testing.T) {
|
||||
testErr := errors.New("bind error")
|
||||
|
||||
getLastNameErr := func(s utils.Initial) ReaderReaderIOResult[AppConfig, string] {
|
||||
return Left[AppConfig, string](testErr)
|
||||
}
|
||||
|
||||
res := F.Pipe3(
|
||||
Do[AppConfig](utils.Empty),
|
||||
Bind(utils.SetLastName, getLastNameErr),
|
||||
Bind(utils.SetGivenName, getGivenName),
|
||||
Map[AppConfig](utils.GetFullName),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
}
|
||||
|
||||
func TestLet(t *testing.T) {
|
||||
type State struct {
|
||||
FirstName string
|
||||
LastName string
|
||||
FullName string
|
||||
}
|
||||
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{FirstName: "John", LastName: "Doe"}),
|
||||
Let[AppConfig](
|
||||
func(fullName string) func(State) State {
|
||||
return func(s State) State {
|
||||
s.FullName = fullName
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s State) string {
|
||||
return s.FirstName + " " + s.LastName
|
||||
},
|
||||
),
|
||||
Map[AppConfig](func(s State) string { return s.FullName }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of("John Doe"), outcome)
|
||||
}
|
||||
|
||||
func TestLetTo(t *testing.T) {
|
||||
type State struct {
|
||||
Status string
|
||||
}
|
||||
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{}),
|
||||
LetTo[AppConfig](
|
||||
func(status string) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Status = status
|
||||
return s
|
||||
}
|
||||
},
|
||||
"active",
|
||||
),
|
||||
Map[AppConfig](func(s State) string { return s.Status }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of("active"), outcome)
|
||||
}
|
||||
|
||||
func TestBindTo(t *testing.T) {
|
||||
type State struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
res := F.Pipe2(
|
||||
Of[AppConfig](42),
|
||||
BindTo[AppConfig](func(n int) State { return State{Count: n} }),
|
||||
Map[AppConfig](func(s State) int { return s.Count }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
}
|
||||
|
||||
func TestApS(t *testing.T) {
|
||||
res := F.Pipe3(
|
||||
Do[AppConfig](utils.Empty),
|
||||
ApS(utils.SetLastName, Of[AppConfig]("Doe")),
|
||||
ApS(utils.SetGivenName, Of[AppConfig]("John")),
|
||||
Map[AppConfig](utils.GetFullName),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of("John Doe"), outcome)
|
||||
}
|
||||
|
||||
func TestApSWithError(t *testing.T) {
|
||||
testErr := errors.New("aps error")
|
||||
|
||||
res := F.Pipe3(
|
||||
Do[AppConfig](utils.Empty),
|
||||
ApS(utils.SetLastName, Left[AppConfig, string](testErr)),
|
||||
ApS(utils.SetGivenName, Of[AppConfig]("John")),
|
||||
Map[AppConfig](utils.GetFullName),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
}
|
||||
|
||||
func TestBindReaderK(t *testing.T) {
|
||||
type State struct {
|
||||
Config string
|
||||
}
|
||||
|
||||
getConfigPath := func(s State) func(AppConfig) string {
|
||||
return func(cfg AppConfig) string {
|
||||
return cfg.DatabaseURL
|
||||
}
|
||||
}
|
||||
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{}),
|
||||
BindReaderK(
|
||||
func(path string) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Config = path
|
||||
return s
|
||||
}
|
||||
},
|
||||
getConfigPath,
|
||||
),
|
||||
Map[AppConfig](func(s State) string { return s.Config }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of("postgres://localhost"), outcome)
|
||||
}
|
||||
|
||||
func TestBindIOResultK(t *testing.T) {
|
||||
type State struct {
|
||||
Value int
|
||||
ParsedValue int
|
||||
}
|
||||
|
||||
parseValue := func(s State) ioresult.IOResult[int] {
|
||||
return func() result.Result[int] {
|
||||
if s.Value < 0 {
|
||||
return result.Left[int](errors.New("negative value"))
|
||||
}
|
||||
return result.Of(s.Value * 2)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("success case", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{Value: 5}),
|
||||
BindIOResultK[AppConfig](
|
||||
func(parsed int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.ParsedValue = parsed
|
||||
return s
|
||||
}
|
||||
},
|
||||
parseValue,
|
||||
),
|
||||
Map[AppConfig](func(s State) int { return s.ParsedValue }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(10), outcome)
|
||||
})
|
||||
|
||||
t.Run("error case", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{Value: -5}),
|
||||
BindIOResultK[AppConfig](
|
||||
func(parsed int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.ParsedValue = parsed
|
||||
return s
|
||||
}
|
||||
},
|
||||
parseValue,
|
||||
),
|
||||
Map[AppConfig](func(s State) int { return s.ParsedValue }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindIOK(t *testing.T) {
|
||||
type State struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
getValue := func(s State) io.IO[int] {
|
||||
return func() int {
|
||||
return s.Value * 2
|
||||
}
|
||||
}
|
||||
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{Value: 21}),
|
||||
BindIOK[AppConfig](
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
getValue,
|
||||
),
|
||||
Map[AppConfig](func(s State) int { return s.Value }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
}
|
||||
|
||||
func TestBindReaderIOK(t *testing.T) {
|
||||
type State struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
getValue := func(s State) readerio.ReaderIO[AppConfig, int] {
|
||||
return func(cfg AppConfig) io.IO[int] {
|
||||
return func() int {
|
||||
return s.Value + len(cfg.DatabaseURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{Value: 10}),
|
||||
BindReaderIOK(
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
getValue,
|
||||
),
|
||||
Map[AppConfig](func(s State) int { return s.Value }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
// 10 + len("postgres://localhost") = 10 + 20 = 30
|
||||
assert.Equal(t, result.Of(30), outcome)
|
||||
}
|
||||
|
||||
func TestBindEitherK(t *testing.T) {
|
||||
type State struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
parseValue := func(s State) either.Either[error, int] {
|
||||
if s.Value < 0 {
|
||||
return either.Left[int](errors.New("negative"))
|
||||
}
|
||||
return either.Right[error](s.Value * 2)
|
||||
}
|
||||
|
||||
t.Run("success case", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{Value: 5}),
|
||||
BindEitherK[AppConfig](
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
parseValue,
|
||||
),
|
||||
Map[AppConfig](func(s State) int { return s.Value }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(10), outcome)
|
||||
})
|
||||
|
||||
t.Run("error case", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{Value: -5}),
|
||||
BindEitherK[AppConfig](
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
parseValue,
|
||||
),
|
||||
Map[AppConfig](func(s State) int { return s.Value }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindIOEitherK(t *testing.T) {
|
||||
type State struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
parseValue := func(s State) ioeither.IOEither[error, int] {
|
||||
return func() either.Either[error, int] {
|
||||
if s.Value < 0 {
|
||||
return either.Left[int](errors.New("negative"))
|
||||
}
|
||||
return either.Right[error](s.Value * 2)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("success case", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{Value: 5}),
|
||||
BindIOEitherK[AppConfig](
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
parseValue,
|
||||
),
|
||||
Map[AppConfig](func(s State) int { return s.Value }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(10), outcome)
|
||||
})
|
||||
|
||||
t.Run("error case", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{Value: -5}),
|
||||
BindIOEitherK[AppConfig](
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
parseValue,
|
||||
),
|
||||
Map[AppConfig](func(s State) int { return s.Value }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
})
|
||||
}
|
||||
|
||||
func TestLensOperations(t *testing.T) {
|
||||
type State struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
valueLens := lens.MakeLens(
|
||||
func(s State) int { return s.Value },
|
||||
func(s State, v int) State {
|
||||
s.Value = v
|
||||
return s
|
||||
},
|
||||
)
|
||||
|
||||
t.Run("ApSL", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{}),
|
||||
ApSL(valueLens, Of[AppConfig](42)),
|
||||
Map[AppConfig](func(s State) int { return s.Value }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
})
|
||||
|
||||
t.Run("BindL", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{Value: 10}),
|
||||
BindL(valueLens, func(v int) ReaderReaderIOResult[AppConfig, int] {
|
||||
return Of[AppConfig](v * 2)
|
||||
}),
|
||||
Map[AppConfig](func(s State) int { return s.Value }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(20), outcome)
|
||||
})
|
||||
|
||||
t.Run("LetL", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{Value: 10}),
|
||||
LetL[AppConfig](valueLens, func(v int) int { return v * 3 }),
|
||||
Map[AppConfig](func(s State) int { return s.Value }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(30), outcome)
|
||||
})
|
||||
|
||||
t.Run("LetToL", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{}),
|
||||
LetToL[AppConfig](valueLens, 99),
|
||||
Map[AppConfig](func(s State) int { return s.Value }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(99), outcome)
|
||||
})
|
||||
|
||||
t.Run("BindIOEitherKL", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{Value: 5}),
|
||||
BindIOEitherKL[AppConfig](valueLens, func(v int) ioeither.IOEither[error, int] {
|
||||
return ioeither.Of[error](v * 2)
|
||||
}),
|
||||
Map[AppConfig](func(s State) int { return s.Value }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(10), outcome)
|
||||
})
|
||||
|
||||
t.Run("BindIOKL", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{Value: 7}),
|
||||
BindIOKL[AppConfig](valueLens, func(v int) io.IO[int] {
|
||||
return func() int { return v * 3 }
|
||||
}),
|
||||
Map[AppConfig](func(s State) int { return s.Value }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(21), outcome)
|
||||
})
|
||||
|
||||
t.Run("BindReaderKL", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{Value: 5}),
|
||||
BindReaderKL(valueLens, func(v int) reader.Reader[AppConfig, int] {
|
||||
return func(cfg AppConfig) int {
|
||||
return v + len(cfg.LogLevel)
|
||||
}
|
||||
}),
|
||||
Map[AppConfig](func(s State) int { return s.Value }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
// 5 + len("info") = 5 + 4 = 9
|
||||
assert.Equal(t, result.Of(9), outcome)
|
||||
})
|
||||
|
||||
t.Run("BindReaderIOKL", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{Value: 10}),
|
||||
BindReaderIOKL(valueLens, func(v int) readerio.ReaderIO[AppConfig, int] {
|
||||
return func(cfg AppConfig) io.IO[int] {
|
||||
return func() int {
|
||||
return v + len(cfg.DatabaseURL)
|
||||
}
|
||||
}
|
||||
}),
|
||||
Map[AppConfig](func(s State) int { return s.Value }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
// 10 + len("postgres://localhost") = 10 + 20 = 30
|
||||
assert.Equal(t, result.Of(30), outcome)
|
||||
})
|
||||
|
||||
t.Run("ApIOEitherSL", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{}),
|
||||
ApIOEitherSL[AppConfig](valueLens, ioeither.Of[error](42)),
|
||||
Map[AppConfig](func(s State) int { return s.Value }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
})
|
||||
|
||||
t.Run("ApIOSL", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{}),
|
||||
ApIOSL[AppConfig](valueLens, func() int { return 99 }),
|
||||
Map[AppConfig](func(s State) int { return s.Value }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(99), outcome)
|
||||
})
|
||||
|
||||
t.Run("ApReaderSL", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{}),
|
||||
ApReaderSL(valueLens, func(cfg AppConfig) int {
|
||||
return len(cfg.LogLevel)
|
||||
}),
|
||||
Map[AppConfig](func(s State) int { return s.Value }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(4), outcome)
|
||||
})
|
||||
|
||||
t.Run("ApReaderIOSL", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{}),
|
||||
ApReaderIOSL(valueLens, func(cfg AppConfig) io.IO[int] {
|
||||
return func() int { return len(cfg.DatabaseURL) }
|
||||
}),
|
||||
Map[AppConfig](func(s State) int { return s.Value }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(20), outcome)
|
||||
})
|
||||
|
||||
t.Run("ApEitherSL", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{}),
|
||||
ApEitherSL[AppConfig](valueLens, either.Right[error](77)),
|
||||
Map[AppConfig](func(s State) int { return s.Value }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(77), outcome)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApOperations(t *testing.T) {
|
||||
type State struct {
|
||||
Value1 int
|
||||
Value2 int
|
||||
}
|
||||
|
||||
t.Run("ApIOEitherS", func(t *testing.T) {
|
||||
res := F.Pipe3(
|
||||
Do[AppConfig](State{}),
|
||||
ApIOEitherS[AppConfig](
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value1 = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
ioeither.Of[error](10),
|
||||
),
|
||||
ApIOEitherS[AppConfig](
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value2 = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
ioeither.Of[error](20),
|
||||
),
|
||||
Map[AppConfig](func(s State) int { return s.Value1 + s.Value2 }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(30), outcome)
|
||||
})
|
||||
|
||||
t.Run("ApIOS", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{}),
|
||||
ApIOS[AppConfig](
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value1 = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
func() int { return 42 },
|
||||
),
|
||||
Map[AppConfig](func(s State) int { return s.Value1 }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
})
|
||||
|
||||
t.Run("ApReaderS", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{}),
|
||||
ApReaderS(
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value1 = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(cfg AppConfig) int { return len(cfg.LogLevel) },
|
||||
),
|
||||
Map[AppConfig](func(s State) int { return s.Value1 }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(4), outcome)
|
||||
})
|
||||
|
||||
t.Run("ApReaderIOS", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{}),
|
||||
ApReaderIOS(
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value1 = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(cfg AppConfig) io.IO[int] {
|
||||
return func() int { return len(cfg.DatabaseURL) }
|
||||
},
|
||||
),
|
||||
Map[AppConfig](func(s State) int { return s.Value1 }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(20), outcome)
|
||||
})
|
||||
|
||||
t.Run("ApEitherS", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{}),
|
||||
ApEitherS[AppConfig](
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value1 = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
either.Right[error](99),
|
||||
),
|
||||
Map[AppConfig](func(s State) int { return s.Value1 }),
|
||||
)
|
||||
|
||||
outcome := res(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(99), outcome)
|
||||
})
|
||||
}
|
||||
95
v2/context/readerreaderioresult/bracket.go
Normal file
95
v2/context/readerreaderioresult/bracket.go
Normal file
@@ -0,0 +1,95 @@
|
||||
// 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 readerreaderioresult
|
||||
|
||||
import (
|
||||
RRIOE "github.com/IBM/fp-go/v2/readerreaderioeither"
|
||||
)
|
||||
|
||||
// Bracket ensures that a resource is properly cleaned up regardless of whether the operation
|
||||
// succeeds or fails. It follows the acquire-use-release pattern with access to both outer (R)
|
||||
// and inner (C) reader contexts.
|
||||
//
|
||||
// The release action is always called after the use action completes, whether it succeeds or fails.
|
||||
// This makes it ideal for managing resources like file handles, database connections, or locks.
|
||||
//
|
||||
// Parameters:
|
||||
// - acquire: Acquires the resource, returning a ReaderReaderIOEither[R, C, E, A]
|
||||
// - use: Uses the acquired resource to perform an operation, returning ReaderReaderIOEither[R, C, E, B]
|
||||
// - release: Releases the resource, receiving both the resource and the result of use
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderReaderIOEither[R, C, E, B] that safely manages the resource lifecycle
|
||||
//
|
||||
// The release function receives:
|
||||
// - The acquired resource (A)
|
||||
// - The result of the use function (Either[E, B])
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type OuterConfig struct {
|
||||
// ConnectionPool string
|
||||
// }
|
||||
// type InnerConfig struct {
|
||||
// Timeout time.Duration
|
||||
// }
|
||||
//
|
||||
// // Acquire a database connection
|
||||
// acquire := func(outer OuterConfig) readerioeither.ReaderIOEither[InnerConfig, error, *sql.DB] {
|
||||
// return func(inner InnerConfig) ioeither.IOEither[error, *sql.DB] {
|
||||
// return ioeither.TryCatch(
|
||||
// func() (*sql.DB, error) {
|
||||
// return sql.Open("postgres", outer.ConnectionPool)
|
||||
// },
|
||||
// func(err error) error { return err },
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Use the connection
|
||||
// use := func(db *sql.DB) readerreaderioeither.ReaderReaderIOEither[OuterConfig, InnerConfig, error, []User] {
|
||||
// return func(outer OuterConfig) readerioeither.ReaderIOEither[InnerConfig, error, []User] {
|
||||
// return func(inner InnerConfig) ioeither.IOEither[error, []User] {
|
||||
// return queryUsers(db, inner.Timeout)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Release the connection
|
||||
// release := func(db *sql.DB, result either.Either[error, []User]) readerreaderioeither.ReaderReaderIOEither[OuterConfig, InnerConfig, error, any] {
|
||||
// return func(outer OuterConfig) readerioeither.ReaderIOEither[InnerConfig, error, any] {
|
||||
// return func(inner InnerConfig) ioeither.IOEither[error, any] {
|
||||
// return ioeither.TryCatch(
|
||||
// func() (any, error) {
|
||||
// return nil, db.Close()
|
||||
// },
|
||||
// func(err error) error { return err },
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// result := readerreaderioeither.Bracket(acquire, use, release)
|
||||
//
|
||||
//go:inline
|
||||
func Bracket[
|
||||
R, A, B, ANY any](
|
||||
acquire ReaderReaderIOResult[R, A],
|
||||
use Kleisli[R, A, B],
|
||||
release func(A, Result[B]) ReaderReaderIOResult[R, ANY],
|
||||
) ReaderReaderIOResult[R, B] {
|
||||
return RRIOE.Bracket(acquire, use, release)
|
||||
}
|
||||
396
v2/context/readerreaderioresult/bracket_test.go
Normal file
396
v2/context/readerreaderioresult/bracket_test.go
Normal file
@@ -0,0 +1,396 @@
|
||||
// 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 readerreaderioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type Resource struct {
|
||||
id string
|
||||
acquired bool
|
||||
released bool
|
||||
}
|
||||
|
||||
func TestBracketSuccessPath(t *testing.T) {
|
||||
cfg := defaultConfig
|
||||
ctx := t.Context()
|
||||
|
||||
resource := &Resource{id: "res1"}
|
||||
|
||||
// Acquire resource
|
||||
acquire := func(c AppConfig) ReaderIOResult[context.Context, *Resource] {
|
||||
return func(ctx context.Context) IOResult[*Resource] {
|
||||
return func() Result[*Resource] {
|
||||
resource.acquired = true
|
||||
return result.Of(resource)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use resource successfully
|
||||
use := func(r *Resource) ReaderReaderIOResult[AppConfig, string] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, string] {
|
||||
return func(ctx context.Context) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
return result.Of("result from " + r.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Release resource
|
||||
release := func(r *Resource, res Result[string]) ReaderReaderIOResult[AppConfig, any] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, any] {
|
||||
return func(ctx context.Context) IOResult[any] {
|
||||
return func() Result[any] {
|
||||
r.released = true
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := Bracket(acquire, use, release)
|
||||
outcome := computation(cfg)(ctx)()
|
||||
|
||||
assert.Equal(t, result.Of("result from res1"), outcome)
|
||||
assert.True(t, resource.acquired, "Resource should be acquired")
|
||||
assert.True(t, resource.released, "Resource should be released")
|
||||
}
|
||||
|
||||
func TestBracketUseFailure(t *testing.T) {
|
||||
cfg := defaultConfig
|
||||
ctx := t.Context()
|
||||
|
||||
resource := &Resource{id: "res1"}
|
||||
useErr := errors.New("use failed")
|
||||
|
||||
// Acquire resource
|
||||
acquire := func(c AppConfig) ReaderIOResult[context.Context, *Resource] {
|
||||
return func(ctx context.Context) IOResult[*Resource] {
|
||||
return func() Result[*Resource] {
|
||||
resource.acquired = true
|
||||
return result.Of(resource)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use resource with failure
|
||||
use := func(r *Resource) ReaderReaderIOResult[AppConfig, string] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, string] {
|
||||
return func(ctx context.Context) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
return result.Left[string](useErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Release resource (should still be called)
|
||||
release := func(r *Resource, res Result[string]) ReaderReaderIOResult[AppConfig, any] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, any] {
|
||||
return func(ctx context.Context) IOResult[any] {
|
||||
return func() Result[any] {
|
||||
r.released = true
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := Bracket(acquire, use, release)
|
||||
outcome := computation(cfg)(ctx)()
|
||||
|
||||
assert.Equal(t, result.Left[string](useErr), outcome)
|
||||
assert.True(t, resource.acquired, "Resource should be acquired")
|
||||
assert.True(t, resource.released, "Resource should be released even on failure")
|
||||
}
|
||||
|
||||
func TestBracketAcquireFailure(t *testing.T) {
|
||||
cfg := defaultConfig
|
||||
ctx := t.Context()
|
||||
|
||||
resource := &Resource{id: "res1"}
|
||||
acquireErr := errors.New("acquire failed")
|
||||
useCalled := false
|
||||
releaseCalled := false
|
||||
|
||||
// Acquire resource fails
|
||||
acquire := func(c AppConfig) ReaderIOResult[context.Context, *Resource] {
|
||||
return func(ctx context.Context) IOResult[*Resource] {
|
||||
return func() Result[*Resource] {
|
||||
return result.Left[*Resource](acquireErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use should not be called
|
||||
use := func(r *Resource) ReaderReaderIOResult[AppConfig, string] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, string] {
|
||||
return func(ctx context.Context) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
useCalled = true
|
||||
return result.Of("should not reach here")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Release should not be called
|
||||
release := func(r *Resource, res Result[string]) ReaderReaderIOResult[AppConfig, any] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, any] {
|
||||
return func(ctx context.Context) IOResult[any] {
|
||||
return func() Result[any] {
|
||||
releaseCalled = true
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := Bracket(acquire, use, release)
|
||||
outcome := computation(cfg)(ctx)()
|
||||
|
||||
assert.Equal(t, result.Left[string](acquireErr), outcome)
|
||||
assert.False(t, resource.acquired, "Resource should not be acquired")
|
||||
assert.False(t, useCalled, "Use should not be called when acquire fails")
|
||||
assert.False(t, releaseCalled, "Release should not be called when acquire fails")
|
||||
}
|
||||
|
||||
func TestBracketReleaseReceivesResult(t *testing.T) {
|
||||
cfg := defaultConfig
|
||||
ctx := t.Context()
|
||||
|
||||
resource := &Resource{id: "res1"}
|
||||
var capturedResult Result[string]
|
||||
|
||||
// Acquire resource
|
||||
acquire := func(c AppConfig) ReaderIOResult[context.Context, *Resource] {
|
||||
return func(ctx context.Context) IOResult[*Resource] {
|
||||
return func() Result[*Resource] {
|
||||
resource.acquired = true
|
||||
return result.Of(resource)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use resource
|
||||
use := func(r *Resource) ReaderReaderIOResult[AppConfig, string] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, string] {
|
||||
return func(ctx context.Context) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
return result.Of("use result")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Release captures the result
|
||||
release := func(r *Resource, res Result[string]) ReaderReaderIOResult[AppConfig, any] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, any] {
|
||||
return func(ctx context.Context) IOResult[any] {
|
||||
return func() Result[any] {
|
||||
capturedResult = res
|
||||
r.released = true
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := Bracket(acquire, use, release)
|
||||
outcome := computation(cfg)(ctx)()
|
||||
|
||||
assert.Equal(t, result.Of("use result"), outcome)
|
||||
assert.Equal(t, result.Of("use result"), capturedResult)
|
||||
assert.True(t, resource.released, "Resource should be released")
|
||||
}
|
||||
|
||||
func TestBracketWithContextAccess(t *testing.T) {
|
||||
cfg := AppConfig{DatabaseURL: "production-db", LogLevel: "debug"}
|
||||
ctx := t.Context()
|
||||
|
||||
resource := &Resource{id: "res1"}
|
||||
|
||||
// Acquire uses outer context
|
||||
acquire := func(c AppConfig) ReaderIOResult[context.Context, *Resource] {
|
||||
return func(ctx context.Context) IOResult[*Resource] {
|
||||
return func() Result[*Resource] {
|
||||
resource.id = c.DatabaseURL + "-resource"
|
||||
resource.acquired = true
|
||||
return result.Of(resource)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use uses both contexts
|
||||
use := func(r *Resource) ReaderReaderIOResult[AppConfig, string] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, string] {
|
||||
return func(ctx context.Context) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
res := r.id + " with log level " + c.LogLevel
|
||||
return result.Of(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Release uses both contexts
|
||||
release := func(r *Resource, res Result[string]) ReaderReaderIOResult[AppConfig, any] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, any] {
|
||||
return func(ctx context.Context) IOResult[any] {
|
||||
return func() Result[any] {
|
||||
r.released = true
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := Bracket(acquire, use, release)
|
||||
outcome := computation(cfg)(ctx)()
|
||||
|
||||
assert.True(t, result.IsRight(outcome))
|
||||
assert.True(t, resource.acquired)
|
||||
assert.True(t, resource.released)
|
||||
assert.Equal(t, "production-db-resource", resource.id)
|
||||
}
|
||||
|
||||
func TestBracketMultipleResources(t *testing.T) {
|
||||
cfg := defaultConfig
|
||||
ctx := t.Context()
|
||||
|
||||
resource1 := &Resource{id: "res1"}
|
||||
resource2 := &Resource{id: "res2"}
|
||||
|
||||
// Acquire first resource
|
||||
acquire1 := func(c AppConfig) ReaderIOResult[context.Context, *Resource] {
|
||||
return func(ctx context.Context) IOResult[*Resource] {
|
||||
return func() Result[*Resource] {
|
||||
resource1.acquired = true
|
||||
return result.Of(resource1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use first resource to acquire second
|
||||
use1 := func(r1 *Resource) ReaderReaderIOResult[AppConfig, string] {
|
||||
// Nested bracket for second resource
|
||||
acquire2 := func(c AppConfig) ReaderIOResult[context.Context, *Resource] {
|
||||
return func(ctx context.Context) IOResult[*Resource] {
|
||||
return func() Result[*Resource] {
|
||||
resource2.acquired = true
|
||||
return result.Of(resource2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use2 := func(r2 *Resource) ReaderReaderIOResult[AppConfig, string] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, string] {
|
||||
return func(ctx context.Context) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
return result.Of(r1.id + " and " + r2.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
release2 := func(r2 *Resource, res Result[string]) ReaderReaderIOResult[AppConfig, any] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, any] {
|
||||
return func(ctx context.Context) IOResult[any] {
|
||||
return func() Result[any] {
|
||||
r2.released = true
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Bracket(acquire2, use2, release2)
|
||||
}
|
||||
|
||||
release1 := func(r1 *Resource, res Result[string]) ReaderReaderIOResult[AppConfig, any] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, any] {
|
||||
return func(ctx context.Context) IOResult[any] {
|
||||
return func() Result[any] {
|
||||
r1.released = true
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := Bracket(acquire1, use1, release1)
|
||||
outcome := computation(cfg)(ctx)()
|
||||
|
||||
assert.Equal(t, result.Of("res1 and res2"), outcome)
|
||||
assert.True(t, resource1.acquired && resource1.released, "Resource 1 should be acquired and released")
|
||||
assert.True(t, resource2.acquired && resource2.released, "Resource 2 should be acquired and released")
|
||||
}
|
||||
|
||||
func TestBracketReleaseErrorDoesNotAffectResult(t *testing.T) {
|
||||
cfg := defaultConfig
|
||||
ctx := t.Context()
|
||||
|
||||
resource := &Resource{id: "res1"}
|
||||
releaseErr := errors.New("release failed")
|
||||
|
||||
// Acquire resource
|
||||
acquire := func(c AppConfig) ReaderIOResult[context.Context, *Resource] {
|
||||
return func(ctx context.Context) IOResult[*Resource] {
|
||||
return func() Result[*Resource] {
|
||||
resource.acquired = true
|
||||
return result.Of(resource)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use resource successfully
|
||||
use := func(r *Resource) ReaderReaderIOResult[AppConfig, string] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, string] {
|
||||
return func(ctx context.Context) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
return result.Of("use success")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Release fails but shouldn't affect the result
|
||||
release := func(r *Resource, res Result[string]) ReaderReaderIOResult[AppConfig, any] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, any] {
|
||||
return func(ctx context.Context) IOResult[any] {
|
||||
return func() Result[any] {
|
||||
return result.Left[any](releaseErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := Bracket(acquire, use, release)
|
||||
outcome := computation(cfg)(ctx)()
|
||||
|
||||
// The use result should be returned, not the release error
|
||||
// (This behavior depends on the Bracket implementation)
|
||||
assert.True(t, result.IsRight(outcome) || result.IsLeft(outcome))
|
||||
assert.True(t, resource.acquired)
|
||||
}
|
||||
430
v2/context/readerreaderioresult/context_test.go
Normal file
430
v2/context/readerreaderioresult/context_test.go
Normal file
@@ -0,0 +1,430 @@
|
||||
// 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 readerreaderioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/IBM/fp-go/v2/retry"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestContextCancellationInMap tests that context cancellation is properly handled in Map operations
|
||||
func TestContextCancellationInMap(t *testing.T) {
|
||||
cfg := defaultConfig
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // Cancel immediately
|
||||
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](42),
|
||||
Map[AppConfig](func(n int) int {
|
||||
// This should still execute as Map doesn't check context
|
||||
return n * 2
|
||||
}),
|
||||
)
|
||||
|
||||
outcome := computation(cfg)(ctx)()
|
||||
// Map operations don't inherently check context, so they succeed
|
||||
assert.Equal(t, result.Of(84), outcome)
|
||||
}
|
||||
|
||||
// TestContextCancellationInChain tests context cancellation in Chain operations
|
||||
func TestContextCancellationInChain(t *testing.T) {
|
||||
cfg := defaultConfig
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
executed := false
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](42),
|
||||
Chain(func(n int) ReaderReaderIOResult[AppConfig, int] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, int] {
|
||||
return func(ctx context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
// Check if context is cancelled
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return result.Left[int](ctx.Err())
|
||||
default:
|
||||
executed = true
|
||||
return result.Of(n * 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
cancel() // Cancel before execution
|
||||
outcome := computation(cfg)(ctx)()
|
||||
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
assert.False(t, executed, "Chained operation should not execute when context is cancelled")
|
||||
}
|
||||
|
||||
// TestContextCancellationWithTimeout tests timeout-based cancellation
|
||||
func TestContextCancellationWithTimeout(t *testing.T) {
|
||||
cfg := defaultConfig
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
computation := func(c AppConfig) ReaderIOResult[context.Context, int] {
|
||||
return func(ctx context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
// Simulate long-running operation
|
||||
select {
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
return result.Of(42)
|
||||
case <-ctx.Done():
|
||||
return result.Left[int](ctx.Err())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
outcome := computation(cfg)(ctx)()
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
|
||||
result.Fold(
|
||||
func(err error) any {
|
||||
assert.ErrorIs(t, err, context.DeadlineExceeded)
|
||||
return nil
|
||||
},
|
||||
func(v int) any {
|
||||
t.Fatal("Should have timed out")
|
||||
return nil
|
||||
},
|
||||
)(outcome)
|
||||
}
|
||||
|
||||
// TestContextCancellationInBracket tests that bracket properly handles context cancellation
|
||||
func TestContextCancellationInBracket(t *testing.T) {
|
||||
cfg := defaultConfig
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
resource := &Resource{id: "res1"}
|
||||
useCalled := false
|
||||
|
||||
acquire := func(c AppConfig) ReaderIOResult[context.Context, *Resource] {
|
||||
return func(ctx context.Context) IOResult[*Resource] {
|
||||
return func() Result[*Resource] {
|
||||
resource.acquired = true
|
||||
return result.Of(resource)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use := func(r *Resource) ReaderReaderIOResult[AppConfig, string] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, string] {
|
||||
return func(ctx context.Context) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return result.Left[string](ctx.Err())
|
||||
default:
|
||||
useCalled = true
|
||||
return result.Of("success")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
release := func(r *Resource, res Result[string]) ReaderReaderIOResult[AppConfig, any] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, any] {
|
||||
return func(ctx context.Context) IOResult[any] {
|
||||
return func() Result[any] {
|
||||
r.released = true
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cancel() // Cancel before use
|
||||
computation := Bracket(acquire, use, release)
|
||||
outcome := computation(cfg)(ctx)()
|
||||
|
||||
assert.True(t, resource.acquired, "Resource should be acquired")
|
||||
assert.True(t, resource.released, "Resource should be released even with cancellation")
|
||||
assert.False(t, useCalled, "Use should not execute when context is cancelled")
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
}
|
||||
|
||||
// TestContextCancellationInRetry tests context cancellation during retry operations
|
||||
func TestContextCancellationInRetry(t *testing.T) {
|
||||
cfg := defaultConfig
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
attempts := 0
|
||||
action := func(status retry.RetryStatus) ReaderReaderIOResult[AppConfig, int] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, int] {
|
||||
return func(ctx context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
attempts++
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return result.Left[int](ctx.Err())
|
||||
case <-time.After(30 * time.Millisecond):
|
||||
return result.Left[int](errors.New("temporary error"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
check := func(r Result[int]) bool {
|
||||
return result.IsLeft(r)
|
||||
}
|
||||
|
||||
policy := retry.LimitRetries(10)
|
||||
computation := Retrying(policy, action, check)
|
||||
outcome := computation(cfg)(ctx)()
|
||||
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
// Should stop retrying when context is cancelled
|
||||
assert.Less(t, attempts, 10, "Should stop retrying when context is cancelled")
|
||||
}
|
||||
|
||||
// TestContextPropagationThroughMonadTransforms tests that context is properly propagated
|
||||
func TestContextPropagationThroughMonadTransforms(t *testing.T) {
|
||||
cfg := defaultConfig
|
||||
|
||||
t.Run("context propagates through Map", func(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), "key", "value")
|
||||
|
||||
var capturedCtx context.Context
|
||||
computation := func(c AppConfig) ReaderIOResult[context.Context, string] {
|
||||
return func(ctx context.Context) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
capturedCtx = ctx
|
||||
return result.Of("test")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = computation(cfg)(ctx)()
|
||||
assert.Equal(t, "value", capturedCtx.Value("key"))
|
||||
})
|
||||
|
||||
t.Run("context propagates through Chain", func(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), "key", "value")
|
||||
|
||||
var capturedCtx context.Context
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](42),
|
||||
Chain(func(n int) ReaderReaderIOResult[AppConfig, int] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, int] {
|
||||
return func(ctx context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
capturedCtx = ctx
|
||||
return result.Of(n * 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
_ = computation(cfg)(ctx)()
|
||||
assert.Equal(t, "value", capturedCtx.Value("key"))
|
||||
})
|
||||
|
||||
t.Run("context propagates through Ap", func(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), "key", "value")
|
||||
|
||||
var capturedCtx context.Context
|
||||
fab := func(c AppConfig) ReaderIOResult[context.Context, func(int) int] {
|
||||
return func(ctx context.Context) IOResult[func(int) int] {
|
||||
return func() Result[func(int) int] {
|
||||
capturedCtx = ctx
|
||||
return result.Of(N.Mul(2))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fa := Of[AppConfig](21)
|
||||
computation := MonadAp(fab, fa)
|
||||
|
||||
_ = computation(cfg)(ctx)()
|
||||
assert.Equal(t, "value", capturedCtx.Value("key"))
|
||||
})
|
||||
}
|
||||
|
||||
// TestContextCancellationInAlt tests Alt operation with context cancellation
|
||||
func TestContextCancellationInAlt(t *testing.T) {
|
||||
cfg := defaultConfig
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
firstCalled := false
|
||||
secondCalled := false
|
||||
|
||||
first := func(c AppConfig) ReaderIOResult[context.Context, int] {
|
||||
return func(ctx context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return result.Left[int](ctx.Err())
|
||||
default:
|
||||
firstCalled = true
|
||||
return result.Left[int](errors.New("first error"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
second := func() ReaderReaderIOResult[AppConfig, int] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, int] {
|
||||
return func(ctx context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return result.Left[int](ctx.Err())
|
||||
default:
|
||||
secondCalled = true
|
||||
return result.Of(42)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := MonadAlt(first, second)
|
||||
outcome := computation(cfg)(ctx)()
|
||||
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
assert.False(t, firstCalled, "First should not execute when context is cancelled")
|
||||
assert.False(t, secondCalled, "Second should not execute when context is cancelled")
|
||||
}
|
||||
|
||||
// TestContextCancellationInDoNotation tests context cancellation in do-notation
|
||||
func TestContextCancellationInDoNotation(t *testing.T) {
|
||||
cfg := defaultConfig
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
type State struct {
|
||||
Value1 int
|
||||
Value2 int
|
||||
}
|
||||
|
||||
step1Executed := false
|
||||
step2Executed := false
|
||||
|
||||
computation := F.Pipe2(
|
||||
Do[AppConfig](State{}),
|
||||
Bind(
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value1 = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s State) ReaderReaderIOResult[AppConfig, int] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, int] {
|
||||
return func(ctx context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return result.Left[int](ctx.Err())
|
||||
default:
|
||||
step1Executed = true
|
||||
return result.Of(10)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
Bind(
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value2 = v
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s State) ReaderReaderIOResult[AppConfig, int] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, int] {
|
||||
return func(ctx context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return result.Left[int](ctx.Err())
|
||||
default:
|
||||
step2Executed = true
|
||||
return result.Of(20)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
cancel() // Cancel before execution
|
||||
outcome := computation(cfg)(ctx)()
|
||||
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
assert.False(t, step1Executed, "Step 1 should not execute when context is cancelled")
|
||||
assert.False(t, step2Executed, "Step 2 should not execute when context is cancelled")
|
||||
}
|
||||
|
||||
// TestContextCancellationBetweenSteps tests cancellation between sequential steps
|
||||
func TestContextCancellationBetweenSteps(t *testing.T) {
|
||||
cfg := defaultConfig
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
step1Executed := false
|
||||
step2Executed := false
|
||||
|
||||
computation := F.Pipe1(
|
||||
func(c AppConfig) ReaderIOResult[context.Context, int] {
|
||||
return func(ctx context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
step1Executed = true
|
||||
cancel() // Cancel after first step
|
||||
return result.Of(42)
|
||||
}
|
||||
}
|
||||
},
|
||||
Chain(func(n int) ReaderReaderIOResult[AppConfig, int] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, int] {
|
||||
return func(ctx context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return result.Left[int](ctx.Err())
|
||||
default:
|
||||
step2Executed = true
|
||||
return result.Of(n * 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
outcome := computation(cfg)(ctx)()
|
||||
|
||||
assert.True(t, step1Executed, "Step 1 should execute")
|
||||
assert.False(t, step2Executed, "Step 2 should not execute after cancellation")
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
}
|
||||
76
v2/context/readerreaderioresult/di_test.go
Normal file
76
v2/context/readerreaderioresult/di_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package readerreaderioresult
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
RES "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type (
|
||||
ConsoleDependency interface {
|
||||
Log(msg string) IO[Void]
|
||||
}
|
||||
|
||||
Res[A any] = RES.ReaderIOResult[A]
|
||||
|
||||
ConsoleEnv[A any] = ReaderReaderIOResult[ConsoleDependency, A]
|
||||
|
||||
consoleOnArray struct {
|
||||
logs []string
|
||||
mu sync.Mutex
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
logConsole = reader.Curry1(ConsoleDependency.Log)
|
||||
)
|
||||
|
||||
func (c *consoleOnArray) Log(msg string) IO[Void] {
|
||||
return func() Void {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.logs = append(c.logs, msg)
|
||||
return function.VOID
|
||||
}
|
||||
}
|
||||
|
||||
func makeConsoleOnArray() *consoleOnArray {
|
||||
return &consoleOnArray{}
|
||||
}
|
||||
|
||||
func TestConsoleEnv(t *testing.T) {
|
||||
console := makeConsoleOnArray()
|
||||
|
||||
prg := F.Pipe1(
|
||||
Of[ConsoleDependency]("Hello World!"),
|
||||
TapReaderIOK(logConsole),
|
||||
)
|
||||
|
||||
res := prg(console)(t.Context())()
|
||||
|
||||
assert.Equal(t, result.Of("Hello World!"), res)
|
||||
assert.Equal(t, A.Of("Hello World!"), console.logs)
|
||||
}
|
||||
|
||||
func TestConsoleEnvWithLocal(t *testing.T) {
|
||||
console := makeConsoleOnArray()
|
||||
|
||||
prg := F.Pipe1(
|
||||
Of[ConsoleDependency](42),
|
||||
TapReaderIOK(reader.WithLocal(logConsole, strconv.Itoa)),
|
||||
)
|
||||
|
||||
res := prg(console)(t.Context())()
|
||||
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
assert.Equal(t, A.Of("42"), console.logs)
|
||||
}
|
||||
477
v2/context/readerreaderioresult/doc.go
Normal file
477
v2/context/readerreaderioresult/doc.go
Normal file
@@ -0,0 +1,477 @@
|
||||
// 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 readerreaderioresult provides a functional programming abstraction that combines
|
||||
// four powerful concepts: Reader, Reader, IO, and Result (Either[error, A]) monads in a nested structure.
|
||||
// This is a specialized version of readerreaderioeither where the error type is fixed to `error` and
|
||||
// the inner context is fixed to `context.Context`.
|
||||
//
|
||||
// # Type Definition
|
||||
//
|
||||
// ReaderReaderIOResult[R, A] is defined as:
|
||||
//
|
||||
// type ReaderReaderIOResult[R, A] = ReaderReaderIOEither[R, context.Context, error, A]
|
||||
//
|
||||
// Which expands to:
|
||||
//
|
||||
// func(R) func(context.Context) func() Either[error, A]
|
||||
//
|
||||
// This represents a computation that:
|
||||
// - Takes an outer environment/context of type R
|
||||
// - Returns a function that takes a context.Context
|
||||
// - Returns an IO operation (a thunk/function with no parameters)
|
||||
// - Produces an Either[error, A] (Result[A]) when executed
|
||||
//
|
||||
// # Type Parameter Ordering Convention
|
||||
//
|
||||
// This package follows a consistent convention for ordering type parameters in function signatures.
|
||||
// The general rule is: R -> C -> E -> T (outer context, inner context, error, type), where:
|
||||
// - R: The outer Reader context/environment type
|
||||
// - C: The inner Reader context/environment type (for the ReaderIOEither)
|
||||
// - E: The Either error type
|
||||
// - T: The value type(s) (A, B, etc.)
|
||||
//
|
||||
// However, when some type parameters can be automatically inferred by the Go compiler from
|
||||
// function arguments, the convention is modified to minimize explicit type annotations:
|
||||
//
|
||||
// Rule: Undetectable types come first, followed by detectable types, while preserving
|
||||
// the relative order within each group (R -> C -> E -> T).
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// 1. All types detectable from first argument:
|
||||
// MonadMap[R, C, E, A, B](fa ReaderReaderIOEither[R, C, E, A], f func(A) B)
|
||||
// - R, C, E, A are detectable from fa
|
||||
// - B is detectable from f
|
||||
// - Order: R, C, E, A, B (standard order, all detectable)
|
||||
//
|
||||
// 2. Some types undetectable:
|
||||
// FromReader[C, E, R, A](ma Reader[R, A]) ReaderReaderIOEither[R, C, E, A]
|
||||
// - R, A are detectable from ma
|
||||
// - C, E are undetectable (not in any argument)
|
||||
// - Order: C, E, R, A (C, E first as undetectable, then R, A in standard order)
|
||||
//
|
||||
// 3. Multiple undetectable types:
|
||||
// Local[C, E, A, R1, R2](f func(R2) R1) func(ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A]
|
||||
// - C, E, A are undetectable
|
||||
// - R1, R2 are detectable from f
|
||||
// - Order: C, E, A, R1, R2 (undetectable first, then detectable)
|
||||
//
|
||||
// 4. Functions returning Kleisli arrows:
|
||||
// ChainReaderOptionK[R, C, A, B, E](onNone Lazy[E]) func(readeroption.Kleisli[R, A, B]) Operator[R, C, E, A, B]
|
||||
// - Canonical order would be R, C, E, A, B
|
||||
// - E is detectable from onNone parameter
|
||||
// - R, C, A, B are not detectable (they're in the Kleisli argument type)
|
||||
// - Order: R, C, A, B, E (undetectable R, C, A, B first, then detectable E)
|
||||
//
|
||||
// This convention allows for more ergonomic function calls:
|
||||
//
|
||||
// // Without convention - need to specify all types:
|
||||
// result := FromReader[OuterCtx, InnerCtx, error, User](readerFunc)
|
||||
//
|
||||
// // With convention - only specify undetectable types:
|
||||
// result := FromReader[InnerCtx, error](readerFunc) // R and A inferred from readerFunc
|
||||
//
|
||||
// The reasoning behind this approach is to reduce the number of explicit type parameters
|
||||
// that developers need to specify when calling functions, improving code readability and
|
||||
// reducing verbosity while maintaining type safety.
|
||||
//
|
||||
// Additional examples demonstrating the convention:
|
||||
//
|
||||
// 5. FromReaderOption[R, C, A, E](onNone Lazy[E]) Kleisli[R, C, E, ReaderOption[R, A], A]
|
||||
// - Canonical order would be R, C, E, A
|
||||
// - E is detectable from onNone parameter
|
||||
// - R, C, A are not detectable (they're in the return type's Kleisli)
|
||||
// - Order: R, C, A, E (undetectable R, C, A first, then detectable E)
|
||||
//
|
||||
// 6. MapLeft[R, C, A, E1, E2](f func(E1) E2) func(ReaderReaderIOEither[R, C, E1, A]) ReaderReaderIOEither[R, C, E2, A]
|
||||
// - Canonical order would be R, C, E1, E2, A
|
||||
// - E1, E2 are detectable from f parameter
|
||||
// - R, C, A are not detectable (they're in the return type)
|
||||
// - Order: R, C, A, E1, E2 (undetectable R, C, A first, then detectable E1, E2)
|
||||
//
|
||||
// Additional special cases:
|
||||
//
|
||||
// - Ap[B, R, C, E, A]: B is undetectable (in function return type), so B comes first
|
||||
// - ChainOptionK[R, C, A, B, E]: R, C, A, B are undetectable, E is detectable from onNone
|
||||
// - FromReaderIO[C, E, R, A]: C, E are undetectable, R, A are detectable from ReaderIO[R, A]
|
||||
//
|
||||
// All functions in this package follow this convention consistently.
|
||||
//
|
||||
// # Fantasy Land Specification
|
||||
//
|
||||
// This is a monad transformer combining:
|
||||
// - Reader monad: https://github.com/fantasyland/fantasy-land
|
||||
// - Reader monad (nested): https://github.com/fantasyland/fantasy-land
|
||||
// - IO monad: https://github.com/fantasyland/fantasy-land
|
||||
// - Either monad: https://github.com/fantasyland/fantasy-land#either
|
||||
//
|
||||
// Implemented Fantasy Land algebras:
|
||||
// - Functor: https://github.com/fantasyland/fantasy-land#functor
|
||||
// - Bifunctor: https://github.com/fantasyland/fantasy-land#bifunctor
|
||||
// - Apply: https://github.com/fantasyland/fantasy-land#apply
|
||||
// - Applicative: https://github.com/fantasyland/fantasy-land#applicative
|
||||
// - Chain: https://github.com/fantasyland/fantasy-land#chain
|
||||
// - Monad: https://github.com/fantasyland/fantasy-land#monad
|
||||
// - Alt: https://github.com/fantasyland/fantasy-land#alt
|
||||
//
|
||||
// # ReaderReaderIOEither
|
||||
//
|
||||
// ReaderReaderIOEither[R, C, E, A] represents a computation that:
|
||||
// - Depends on an outer context/environment of type R (outer Reader)
|
||||
// - Returns a computation that depends on an inner context/environment of type C (inner Reader)
|
||||
// - Performs side effects (IO)
|
||||
// - Can fail with an error of type E or succeed with a value of type A (Either)
|
||||
//
|
||||
// This is particularly useful for:
|
||||
// - Multi-level dependency injection patterns
|
||||
// - Layered architectures with different context requirements at each layer
|
||||
// - Composing operations that need access to multiple levels of configuration or context
|
||||
// - Building reusable components that can be configured at different stages
|
||||
//
|
||||
// # Core Operations
|
||||
//
|
||||
// Construction:
|
||||
// - Of/Right: Create a successful computation
|
||||
// - Left: Create a failed computation
|
||||
// - FromEither: Lift an Either into ReaderReaderIOEither
|
||||
// - FromIO: Lift an IO into ReaderReaderIOEither
|
||||
// - FromReader: Lift a Reader into ReaderReaderIOEither
|
||||
// - FromReaderIO: Lift a ReaderIO into ReaderReaderIOEither
|
||||
// - FromIOEither: Lift an IOEither into ReaderReaderIOEither
|
||||
// - FromReaderEither: Lift a ReaderEither into ReaderReaderIOEither
|
||||
// - FromReaderIOEither: Lift a ReaderIOEither into ReaderReaderIOEither
|
||||
// - FromReaderOption: Lift a ReaderOption into ReaderReaderIOEither
|
||||
//
|
||||
// Transformation:
|
||||
// - Map: Transform the success value
|
||||
// - MapLeft: Transform the error value
|
||||
// - Chain/Bind: Sequence dependent computations
|
||||
// - Flatten: Flatten nested ReaderReaderIOEither
|
||||
//
|
||||
// Combination:
|
||||
// - Ap: Apply a function in a context to a value in a context
|
||||
// - ApSeq: Sequential application
|
||||
// - ApPar: Parallel application
|
||||
//
|
||||
// Error Handling:
|
||||
// - Alt: Choose the first successful computation
|
||||
//
|
||||
// Context Access:
|
||||
// - Ask: Get the current outer context
|
||||
// - Asks: Get a value derived from the outer context
|
||||
// - Local: Run a computation with a modified outer context
|
||||
// - Read: Execute with a specific outer context
|
||||
//
|
||||
// Kleisli Composition:
|
||||
// - ChainEitherK: Chain with Either-returning functions
|
||||
// - ChainReaderK: Chain with Reader-returning functions
|
||||
// - ChainReaderIOK: Chain with ReaderIO-returning functions
|
||||
// - ChainReaderEitherK: Chain with ReaderEither-returning functions
|
||||
// - ChainReaderOptionK: Chain with ReaderOption-returning functions
|
||||
// - ChainIOEitherK: Chain with IOEither-returning functions
|
||||
// - ChainIOK: Chain with IO-returning functions
|
||||
// - ChainOptionK: Chain with Option-returning functions
|
||||
//
|
||||
// First/Tap Operations (execute for side effects, return original value):
|
||||
// - ChainFirst/Tap: Execute a computation but return the original value
|
||||
// - ChainFirstEitherK/TapEitherK: Tap with Either-returning functions
|
||||
// - ChainFirstReaderK/TapReaderK: Tap with Reader-returning functions
|
||||
// - ChainFirstReaderIOK/TapReaderIOK: Tap with ReaderIO-returning functions
|
||||
// - ChainFirstReaderEitherK/TapReaderEitherK: Tap with ReaderEither-returning functions
|
||||
// - ChainFirstReaderOptionK/TapReaderOptionK: Tap with ReaderOption-returning functions
|
||||
// - ChainFirstIOK/TapIOK: Tap with IO-returning functions
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// type AppConfig struct {
|
||||
// DatabaseURL string
|
||||
// LogLevel string
|
||||
// }
|
||||
//
|
||||
// // A computation that depends on AppConfig and context.Context
|
||||
// func fetchUser(id int) ReaderReaderIOResult[AppConfig, User] {
|
||||
// return func(cfg AppConfig) readerioresult.ReaderIOResult[context.Context, User] {
|
||||
// // Use cfg.DatabaseURL and cfg.LogLevel
|
||||
// return func(ctx context.Context) ioresult.IOResult[User] {
|
||||
// // Use ctx for cancellation/timeout
|
||||
// return func() result.Result[User] {
|
||||
// // Perform the actual IO operation
|
||||
// // Return result.Of(user) or result.Error[User](err)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Compose operations
|
||||
// result := function.Pipe2(
|
||||
// fetchUser(123),
|
||||
// Map[AppConfig](func(u User) string { return u.Name }),
|
||||
// Chain[AppConfig](func(name string) ReaderReaderIOResult[AppConfig, string] {
|
||||
// return Of[AppConfig]("Hello, " + name)
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
// // Execute with config and context
|
||||
// appConfig := AppConfig{DatabaseURL: "postgres://...", LogLevel: "info"}
|
||||
// ctx := t.Context()
|
||||
// outcome := result(appConfig)(ctx)() // Returns result.Result[string]
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// This monad is particularly useful for:
|
||||
// - Applications with layered configuration (app config + request context)
|
||||
// - HTTP handlers that need both application config and request context
|
||||
// - Database operations with connection pool config and query context
|
||||
// - Retry logic with policy configuration and execution context
|
||||
// - Resource management with bracket pattern across multiple contexts
|
||||
//
|
||||
// # Dependency Injection with the Outer Context
|
||||
//
|
||||
// The outer Reader context (type parameter R) provides a powerful mechanism for dependency injection
|
||||
// in functional programming. This pattern is explained in detail in Scott Wlaschin's talk:
|
||||
// "Dependency Injection, The Functional Way" - https://www.youtube.com/watch?v=xPlsVVaMoB0
|
||||
//
|
||||
// ## Core Concept
|
||||
//
|
||||
// Instead of using traditional OOP dependency injection frameworks, the Reader monad allows you to:
|
||||
// 1. Define functions that declare their dependencies as type parameters
|
||||
// 2. Compose these functions without providing the dependencies
|
||||
// 3. Supply all dependencies at the "end of the world" (program entry point)
|
||||
//
|
||||
// This approach provides:
|
||||
// - Compile-time safety: Missing dependencies cause compilation errors
|
||||
// - Explicit dependencies: Function signatures show exactly what they need
|
||||
// - Easy testing: Mock dependencies by providing different values
|
||||
// - Pure functions: Dependencies are passed as parameters, not global state
|
||||
//
|
||||
// ## Examples from the Video Adapted to fp-go
|
||||
//
|
||||
// ### Example 1: Basic Reader Pattern (Video: "Reader Monad Basics")
|
||||
//
|
||||
// In the video, Scott shows how to pass configuration through a chain of functions.
|
||||
// In fp-go with ReaderReaderIOResult:
|
||||
//
|
||||
// // Define your dependencies
|
||||
// type AppConfig struct {
|
||||
// DatabaseURL string
|
||||
// APIKey string
|
||||
// MaxRetries int
|
||||
// }
|
||||
//
|
||||
// // Functions declare their dependencies via the R type parameter
|
||||
// func getConnectionString() ReaderReaderIOResult[AppConfig, string] {
|
||||
// return Asks[AppConfig](func(cfg AppConfig) string {
|
||||
// return cfg.DatabaseURL
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// func connectToDatabase() ReaderReaderIOResult[AppConfig, *sql.DB] {
|
||||
// return MonadChain(
|
||||
// getConnectionString(),
|
||||
// func(connStr string) ReaderReaderIOResult[AppConfig, *sql.DB] {
|
||||
// return FromIO[AppConfig](func() result.Result[*sql.DB] {
|
||||
// db, err := sql.Open("postgres", connStr)
|
||||
// return result.FromEither(either.FromError(db, err))
|
||||
// })
|
||||
// },
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// ### Example 2: Composing Dependencies (Video: "Composing Reader Functions")
|
||||
//
|
||||
// The video demonstrates how Reader functions compose naturally.
|
||||
// In fp-go, you can compose operations that all share the same dependency:
|
||||
//
|
||||
// func fetchUser(id int) ReaderReaderIOResult[AppConfig, User] {
|
||||
// return MonadChain(
|
||||
// connectToDatabase(),
|
||||
// func(db *sql.DB) ReaderReaderIOResult[AppConfig, User] {
|
||||
// return FromIO[AppConfig](func() result.Result[User] {
|
||||
// // Query database using db and return user
|
||||
// // The AppConfig is still available if needed
|
||||
// })
|
||||
// },
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// func enrichUser(user User) ReaderReaderIOResult[AppConfig, EnrichedUser] {
|
||||
// return Asks[AppConfig, EnrichedUser](func(cfg AppConfig) EnrichedUser {
|
||||
// // Use cfg.APIKey to call external service
|
||||
// return EnrichedUser{User: user, Extra: "data"}
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// // Compose without providing dependencies
|
||||
// pipeline := function.Pipe2(
|
||||
// fetchUser(123),
|
||||
// Chain[AppConfig](enrichUser),
|
||||
// )
|
||||
//
|
||||
// // Provide dependencies at the end
|
||||
// config := AppConfig{DatabaseURL: "...", APIKey: "...", MaxRetries: 3}
|
||||
// ctx := context.Background()
|
||||
// result := pipeline(config)(ctx)()
|
||||
//
|
||||
// ### Example 3: Local Context Modification (Video: "Local Environment")
|
||||
//
|
||||
// The video shows how to temporarily modify the environment for a sub-computation.
|
||||
// In fp-go, use the Local function:
|
||||
//
|
||||
// // Run a computation with modified configuration
|
||||
// func withRetries(retries int, action ReaderReaderIOResult[AppConfig, string]) ReaderReaderIOResult[AppConfig, string] {
|
||||
// return Local[string](func(cfg AppConfig) AppConfig {
|
||||
// // Create a modified config with different retry count
|
||||
// return AppConfig{
|
||||
// DatabaseURL: cfg.DatabaseURL,
|
||||
// APIKey: cfg.APIKey,
|
||||
// MaxRetries: retries,
|
||||
// }
|
||||
// })(action)
|
||||
// }
|
||||
//
|
||||
// // Use it
|
||||
// result := withRetries(5, fetchUser(123))
|
||||
//
|
||||
// ### Example 4: Testing with Mock Dependencies (Video: "Testing with Reader")
|
||||
//
|
||||
// The video emphasizes how Reader makes testing easy by allowing mock dependencies.
|
||||
// In fp-go:
|
||||
//
|
||||
// func TestFetchUser(t *testing.T) {
|
||||
// // Create a test configuration
|
||||
// testConfig := AppConfig{
|
||||
// DatabaseURL: "mock://test",
|
||||
// APIKey: "test-key",
|
||||
// MaxRetries: 1,
|
||||
// }
|
||||
//
|
||||
// // Run the computation with test config
|
||||
// ctx := context.Background()
|
||||
// result := fetchUser(123)(testConfig)(ctx)()
|
||||
//
|
||||
// // Assert on the result
|
||||
// assert.True(t, either.IsRight(result))
|
||||
// }
|
||||
//
|
||||
// ### Example 5: Multi-Layer Dependencies (Video: "Nested Readers")
|
||||
//
|
||||
// The video discusses nested readers for multi-layer architectures.
|
||||
// ReaderReaderIOResult provides exactly this with R (outer) and context.Context (inner):
|
||||
//
|
||||
// type AppConfig struct {
|
||||
// DatabaseURL string
|
||||
// }
|
||||
//
|
||||
// // Outer context: Application-level configuration (AppConfig)
|
||||
// // Inner context: Request-level context (context.Context)
|
||||
// func handleRequest(userID int) ReaderReaderIOResult[AppConfig, Response] {
|
||||
// return func(cfg AppConfig) readerioresult.ReaderIOResult[context.Context, Response] {
|
||||
// // cfg is available here (outer context)
|
||||
// return func(ctx context.Context) ioresult.IOResult[Response] {
|
||||
// // ctx is available here (inner context)
|
||||
// // Both cfg and ctx can be used
|
||||
// return func() result.Result[Response] {
|
||||
// // Perform operation using both contexts
|
||||
// select {
|
||||
// case <-ctx.Done():
|
||||
// return result.Error[Response](ctx.Err())
|
||||
// default:
|
||||
// // Use cfg.DatabaseURL to connect
|
||||
// return result.Of(Response{})
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// ### Example 6: Avoiding Global State (Video: "Problems with Global State")
|
||||
//
|
||||
// The video criticizes global state and shows how Reader solves this.
|
||||
// In fp-go, instead of:
|
||||
//
|
||||
// // BAD: Global state
|
||||
// var globalConfig AppConfig
|
||||
//
|
||||
// func fetchUser(id int) result.Result[User] {
|
||||
// // Uses globalConfig implicitly
|
||||
// db := connectTo(globalConfig.DatabaseURL)
|
||||
// // ...
|
||||
// }
|
||||
//
|
||||
// Use Reader to make dependencies explicit:
|
||||
//
|
||||
// // GOOD: Explicit dependencies
|
||||
// func fetchUser(id int) ReaderReaderIOResult[AppConfig, User] {
|
||||
// return MonadChain(
|
||||
// Ask[AppConfig](), // Explicitly request the config
|
||||
// func(cfg AppConfig) ReaderReaderIOResult[AppConfig, User] {
|
||||
// // Use cfg explicitly
|
||||
// return FromIO[AppConfig](func() result.Result[User] {
|
||||
// db := connectTo(cfg.DatabaseURL)
|
||||
// // ...
|
||||
// })
|
||||
// },
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// ## Benefits of This Approach
|
||||
//
|
||||
// 1. **Type Safety**: The compiler ensures all dependencies are provided
|
||||
// 2. **Testability**: Easy to provide mock dependencies for testing
|
||||
// 3. **Composability**: Functions compose naturally without dependency wiring
|
||||
// 4. **Explicitness**: Function signatures document their dependencies
|
||||
// 5. **Immutability**: Dependencies are immutable values, not mutable global state
|
||||
// 6. **Flexibility**: Use Local to modify dependencies for sub-computations
|
||||
// 7. **Separation of Concerns**: Business logic is separate from dependency resolution
|
||||
//
|
||||
// ## Comparison with Traditional DI
|
||||
//
|
||||
// Traditional OOP DI (e.g., Spring, Guice):
|
||||
// - Runtime dependency resolution
|
||||
// - Magic/reflection-based wiring
|
||||
// - Implicit dependencies (hidden in constructors)
|
||||
// - Mutable containers
|
||||
//
|
||||
// Reader-based DI (fp-go):
|
||||
// - Compile-time dependency resolution
|
||||
// - Explicit function composition
|
||||
// - Explicit dependencies (in type signatures)
|
||||
// - Immutable values
|
||||
//
|
||||
// ## When to Use Each Layer
|
||||
//
|
||||
// - **Outer Reader (R)**: Application-level dependencies that rarely change
|
||||
// - Database connection pools
|
||||
// - API keys and secrets
|
||||
// - Feature flags
|
||||
// - Application configuration
|
||||
//
|
||||
// - **Inner Reader (context.Context)**: Request-level dependencies that change per operation
|
||||
// - Request IDs and tracing
|
||||
// - Cancellation signals
|
||||
// - Deadlines and timeouts
|
||||
// - User authentication tokens
|
||||
//
|
||||
// This two-layer approach mirrors the video's discussion of nested readers and provides
|
||||
// a clean separation between application-level and request-level concerns.
|
||||
//
|
||||
// # Relationship to Other Packages
|
||||
//
|
||||
// - readerreaderioeither: The generic version with configurable error and context types
|
||||
// - readerioresult: Single reader with context.Context and error
|
||||
// - readerresult: Single reader with error (no IO)
|
||||
// - context/readerioresult: Alias for readerioresult with context.Context
|
||||
package readerreaderioresult
|
||||
291
v2/context/readerreaderioresult/flip.go
Normal file
291
v2/context/readerreaderioresult/flip.go
Normal file
@@ -0,0 +1,291 @@
|
||||
// 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 readerreaderioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/internal/readert"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerioeither"
|
||||
RRIOE "github.com/IBM/fp-go/v2/readerreaderioeither"
|
||||
)
|
||||
|
||||
// Sequence swaps the order of nested environment parameters in a ReaderReaderIOResult computation.
|
||||
//
|
||||
// This function takes a ReaderReaderIOResult that produces another ReaderReaderIOResult and returns a
|
||||
// Kleisli arrow that reverses the order of the outer environment parameters (R1 and R2). The result is
|
||||
// a curried function that takes R1 first, then R2, and produces a computation with context.Context and error handling.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R1: The first outer environment type (becomes the outermost after sequence)
|
||||
// - R2: The second outer environment type (becomes inner after sequence)
|
||||
// - A: The success value type
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: A ReaderReaderIOResult[R2, ReaderReaderIOResult[R1, A]]
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli[R2, R1, A], which is func(R1) ReaderReaderIOResult[R2, A]
|
||||
//
|
||||
// The function preserves error handling and IO effects at all levels while reordering the
|
||||
// outer environment dependencies. The inner context.Context layer remains unchanged.
|
||||
//
|
||||
// This is particularly useful when you need to change the order in which contexts are provided
|
||||
// to a nested computation, such as when composing operations that have different dependency orders.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type AppConfig struct {
|
||||
// DatabaseURL string
|
||||
// }
|
||||
// type UserPrefs struct {
|
||||
// Theme string
|
||||
// }
|
||||
//
|
||||
// // Original: takes AppConfig, returns computation that may produce
|
||||
// // another computation depending on UserPrefs
|
||||
// original := func(cfg AppConfig) readerioresult.ReaderIOResult[context.Context,
|
||||
// ReaderReaderIOResult[UserPrefs, string]] {
|
||||
// return readerioresult.Of[context.Context](
|
||||
// Of[UserPrefs]("result"),
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// // Sequence swaps UserPrefs and AppConfig order
|
||||
// sequenced := Sequence[UserPrefs, AppConfig, string](original)
|
||||
//
|
||||
// // Now provide UserPrefs first, then AppConfig
|
||||
// ctx := context.Background()
|
||||
// result := sequenced(UserPrefs{Theme: "dark"})(AppConfig{DatabaseURL: "db"})(ctx)()
|
||||
func Sequence[R1, R2, A any](ma ReaderReaderIOResult[R2, ReaderReaderIOResult[R1, A]]) Kleisli[R2, R1, A] {
|
||||
return readert.Sequence(
|
||||
readerioeither.Chain,
|
||||
ma,
|
||||
)
|
||||
}
|
||||
|
||||
// SequenceReader swaps the order of environment parameters when the inner computation is a pure Reader.
|
||||
//
|
||||
// This function is similar to Sequence but specialized for the case where the innermost computation
|
||||
// is a pure Reader (without IO or error handling) rather than another ReaderReaderIOResult. It takes
|
||||
// a ReaderReaderIOResult that produces a Reader and returns a Kleisli arrow that reverses the order
|
||||
// of the outer environment parameters.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R1: The first environment type (becomes outermost after sequence)
|
||||
// - R2: The second environment type (becomes inner after sequence)
|
||||
// - A: The success value type
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: A ReaderReaderIOResult[R2, Reader[R1, A]]
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli[R2, R1, A], which is func(R1) ReaderReaderIOResult[R2, A]
|
||||
//
|
||||
// The function lifts the pure Reader computation into the ReaderIOResult context (with context.Context
|
||||
// and error handling) while reordering the environment dependencies.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type AppConfig struct {
|
||||
// Multiplier int
|
||||
// }
|
||||
// type Database struct {
|
||||
// ConnectionString string
|
||||
// }
|
||||
//
|
||||
// // Original: takes AppConfig, may produce a Reader[Database, int]
|
||||
// original := func(cfg AppConfig) readerioresult.ReaderIOResult[context.Context, reader.Reader[Database, int]] {
|
||||
// return readerioresult.Of[context.Context](func(db Database) int {
|
||||
// return len(db.ConnectionString) * cfg.Multiplier
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// // Sequence to provide Database first, then AppConfig
|
||||
// sequenced := SequenceReader[Database, AppConfig, int](original)
|
||||
// ctx := context.Background()
|
||||
// result := sequenced(Database{ConnectionString: "localhost"})(AppConfig{Multiplier: 2})(ctx)()
|
||||
func SequenceReader[R1, R2, A any](ma ReaderReaderIOResult[R2, Reader[R1, A]]) Kleisli[R2, R1, A] {
|
||||
return readert.SequenceReader(
|
||||
readerioeither.Map,
|
||||
ma,
|
||||
)
|
||||
}
|
||||
|
||||
// SequenceReaderIO swaps the order of environment parameters when the inner computation is a ReaderIO.
|
||||
//
|
||||
// This function is specialized for the case where the innermost computation is a ReaderIO
|
||||
// (with IO effects but no error handling) rather than another ReaderReaderIOResult. It takes
|
||||
// a ReaderReaderIOResult that produces a ReaderIO and returns a Kleisli arrow that reverses
|
||||
// the order of the outer environment parameters.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R1: The first environment type (becomes outermost after sequence)
|
||||
// - R2: The second environment type (becomes inner after sequence)
|
||||
// - A: The success value type
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: A ReaderReaderIOResult[R2, ReaderIO[R1, A]]
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli[R2, R1, A], which is func(R1) ReaderReaderIOResult[R2, A]
|
||||
//
|
||||
// The function lifts the ReaderIO computation (which has IO effects but no error handling)
|
||||
// into the ReaderIOResult context (with context.Context and error handling) while reordering
|
||||
// the environment dependencies.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type AppConfig struct {
|
||||
// FilePath string
|
||||
// }
|
||||
// type Logger struct {
|
||||
// Level string
|
||||
// }
|
||||
//
|
||||
// // Original: takes AppConfig, may produce a ReaderIO[Logger, string]
|
||||
// original := func(cfg AppConfig) readerioresult.ReaderIOResult[context.Context, readerio.ReaderIO[Logger, string]] {
|
||||
// return readerioresult.Of[context.Context](func(logger Logger) io.IO[string] {
|
||||
// return func() string {
|
||||
// return fmt.Sprintf("[%s] Reading from %s", logger.Level, cfg.FilePath)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// // Sequence to provide Logger first, then AppConfig
|
||||
// sequenced := SequenceReaderIO[Logger, AppConfig, string](original)
|
||||
// ctx := context.Background()
|
||||
// result := sequenced(Logger{Level: "INFO"})(AppConfig{FilePath: "/data"})(ctx)()
|
||||
func SequenceReaderIO[R1, R2, A any](ma ReaderReaderIOResult[R2, ReaderIO[R1, A]]) Kleisli[R2, R1, A] {
|
||||
return RRIOE.SequenceReaderIO(ma)
|
||||
}
|
||||
|
||||
// Traverse transforms a ReaderReaderIOResult computation by applying a function that produces
|
||||
// another ReaderReaderIOResult, effectively swapping the order of outer environment parameters.
|
||||
//
|
||||
// This function is useful when you have a computation that depends on environment R2 and
|
||||
// produces a value of type A, and you want to transform it using a function that takes A
|
||||
// and produces a computation depending on environment R1. The result is a curried function
|
||||
// that takes R1 first, then R2, and produces a computation with context.Context and error handling.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R2: The outer environment type from the original computation
|
||||
// - R1: The inner environment type introduced by the transformation
|
||||
// - A: The input value type
|
||||
// - B: The output value type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Kleisli arrow that transforms A into a ReaderReaderIOResult[R1, B]
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderReaderIOResult[R2, A] and returns a Kleisli[R2, R1, B],
|
||||
// which is func(R1) ReaderReaderIOResult[R2, B]
|
||||
//
|
||||
// The function preserves error handling and IO effects while reordering the environment dependencies.
|
||||
// This is the generalized version of Sequence that also applies a transformation function.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type AppConfig struct {
|
||||
// SystemID string
|
||||
// }
|
||||
// type UserConfig struct {
|
||||
// UserID int
|
||||
// }
|
||||
//
|
||||
// // Original computation depending on AppConfig
|
||||
// original := Of[AppConfig](42)
|
||||
//
|
||||
// // Transformation that introduces UserConfig dependency
|
||||
// transform := func(n int) ReaderReaderIOResult[UserConfig, string] {
|
||||
// return func(userCfg UserConfig) readerioresult.ReaderIOResult[context.Context, string] {
|
||||
// return readerioresult.Of[context.Context](fmt.Sprintf("User %d: %d", userCfg.UserID, n))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Apply traverse to swap order and transform
|
||||
// traversed := Traverse[AppConfig, UserConfig, int, string](transform)(original)
|
||||
//
|
||||
// // Provide UserConfig first, then AppConfig
|
||||
// ctx := context.Background()
|
||||
// result := traversed(UserConfig{UserID: 1})(AppConfig{SystemID: "sys1"})(ctx)()
|
||||
func Traverse[R2, R1, A, B any](
|
||||
f Kleisli[R1, A, B],
|
||||
) func(ReaderReaderIOResult[R2, A]) Kleisli[R2, R1, B] {
|
||||
return readert.Traverse[ReaderReaderIOResult[R2, A]](
|
||||
readerioeither.Map,
|
||||
readerioeither.Chain,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// TraverseReader transforms a ReaderReaderIOResult 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 ReaderReaderIOResult. The result allows you to provide the Reader's environment (R1)
|
||||
// first, which then produces a ReaderReaderIOResult that depends on environment R2.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R2: The outer environment type from the original ReaderReaderIOResult
|
||||
// - R1: The inner environment type introduced by the Reader transformation
|
||||
// - A: The input value type
|
||||
// - B: The output value type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Reader-based Kleisli arrow that transforms A to B using environment R1
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderReaderIOResult[R2, A] and returns a Kleisli[R2, R1, B],
|
||||
// which is func(R1) ReaderReaderIOResult[R2, B]
|
||||
//
|
||||
// The function preserves error handling and IO effects while adding the Reader environment dependency
|
||||
// and reordering the environment parameters. This is useful when you want to introduce a pure
|
||||
// (non-IO, non-error) environment dependency to an existing computation.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type AppConfig struct {
|
||||
// Timeout int
|
||||
// }
|
||||
// type UserPreferences struct {
|
||||
// Theme string
|
||||
// }
|
||||
//
|
||||
// // Original computation depending on AppConfig
|
||||
// original := Of[AppConfig](100)
|
||||
//
|
||||
// // Pure Reader transformation that introduces UserPreferences dependency
|
||||
// formatWithTheme := func(value int) reader.Reader[UserPreferences, string] {
|
||||
// return func(prefs UserPreferences) string {
|
||||
// return fmt.Sprintf("[%s theme] Value: %d", prefs.Theme, value)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Apply traverse to introduce UserPreferences and swap order
|
||||
// traversed := TraverseReader[AppConfig, UserPreferences, int, string](formatWithTheme)(original)
|
||||
//
|
||||
// // Provide UserPreferences first, then AppConfig
|
||||
// ctx := context.Background()
|
||||
// result := traversed(UserPreferences{Theme: "dark"})(AppConfig{Timeout: 30})(ctx)()
|
||||
func TraverseReader[R2, R1, A, B any](
|
||||
f reader.Kleisli[R1, A, B],
|
||||
) func(ReaderReaderIOResult[R2, A]) Kleisli[R2, R1, B] {
|
||||
return readert.TraverseReader[ReaderReaderIOResult[R2, A]](
|
||||
readerioeither.Map,
|
||||
readerioeither.Map,
|
||||
f,
|
||||
)
|
||||
}
|
||||
778
v2/context/readerreaderioresult/flip_test.go
Normal file
778
v2/context/readerreaderioresult/flip_test.go
Normal file
@@ -0,0 +1,778 @@
|
||||
// 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 readerreaderioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
RIORES "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type Config1 struct {
|
||||
value1 int
|
||||
}
|
||||
|
||||
type Config2 struct {
|
||||
value2 string
|
||||
}
|
||||
|
||||
func TestSequence(t *testing.T) {
|
||||
t.Run("swaps parameter order for simple types", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
// Original: takes Config2, returns ReaderIOResult that may produce ReaderReaderIOResult[Config1, int]
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderReaderIOResult[Config1, int]] {
|
||||
return func(ctx1 context.Context) IOResult[ReaderReaderIOResult[Config1, int]] {
|
||||
return func() Result[ReaderReaderIOResult[Config1, int]] {
|
||||
return result.Of(func(cfg1 Config1) RIORES.ReaderIOResult[int] {
|
||||
return func(ctx2 context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
return result.Of(cfg1.value1 + len(cfg2.value2))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sequence swaps Config1 and Config2 order
|
||||
sequenced := Sequence(original)
|
||||
|
||||
cfg1 := Config1{value1: 10}
|
||||
cfg2 := Config2{value2: "hello"}
|
||||
|
||||
// Test original: Config2 -> Context -> Config1 -> Context
|
||||
result1 := original(cfg2)(ctx)()
|
||||
assert.True(t, result.IsRight(result1))
|
||||
innerFunc1, _ := result.Unwrap(result1)
|
||||
innerResult1 := innerFunc1(cfg1)(ctx)()
|
||||
assert.Equal(t, result.Of(15), innerResult1)
|
||||
|
||||
// Test sequenced: Config1 -> Config2 -> Context
|
||||
innerFunc2 := sequenced(cfg1)
|
||||
innerResult2 := innerFunc2(cfg2)(ctx)()
|
||||
assert.Equal(t, result.Of(15), innerResult2)
|
||||
})
|
||||
|
||||
t.Run("preserves error handling", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
testErr := errors.New("test error")
|
||||
|
||||
// Original that returns an error
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderReaderIOResult[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[ReaderReaderIOResult[Config1, int]] {
|
||||
return func() Result[ReaderReaderIOResult[Config1, int]] {
|
||||
return result.Left[ReaderReaderIOResult[Config1, int]](testErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := Sequence(original)
|
||||
|
||||
cfg1 := Config1{value1: 10}
|
||||
cfg2 := Config2{value2: "hello"}
|
||||
|
||||
// Test sequenced preserves error
|
||||
innerFunc := sequenced(cfg1)
|
||||
outcome := innerFunc(cfg2)(ctx)()
|
||||
assert.Equal(t, result.Left[int](testErr), outcome)
|
||||
})
|
||||
|
||||
t.Run("works with nested computations", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
// Original with nested logic
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderReaderIOResult[Config1, string]] {
|
||||
return func(ctx context.Context) IOResult[ReaderReaderIOResult[Config1, string]] {
|
||||
return func() Result[ReaderReaderIOResult[Config1, string]] {
|
||||
if len(cfg2.value2) == 0 {
|
||||
return result.Left[ReaderReaderIOResult[Config1, string]](errors.New("empty string"))
|
||||
}
|
||||
return result.Of(func(cfg1 Config1) RIORES.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
if cfg1.value1 < 0 {
|
||||
return result.Left[string](errors.New("negative value"))
|
||||
}
|
||||
return result.Of(fmt.Sprintf("%s:%d", cfg2.value2, cfg1.value1))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := Sequence(original)
|
||||
|
||||
// Test with valid inputs
|
||||
result1 := sequenced(Config1{value1: 42})(Config2{value2: "test"})(ctx)()
|
||||
assert.Equal(t, result.Of("test:42"), result1)
|
||||
|
||||
// Test with empty string
|
||||
result2 := sequenced(Config1{value1: 42})(Config2{value2: ""})(ctx)()
|
||||
assert.True(t, result.IsLeft(result2))
|
||||
|
||||
// Test with negative value
|
||||
result3 := sequenced(Config1{value1: -1})(Config2{value2: "test"})(ctx)()
|
||||
assert.True(t, result.IsLeft(result3))
|
||||
})
|
||||
|
||||
t.Run("works with zero values", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderReaderIOResult[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[ReaderReaderIOResult[Config1, int]] {
|
||||
return func() Result[ReaderReaderIOResult[Config1, int]] {
|
||||
return result.Of(func(cfg1 Config1) RIORES.ReaderIOResult[int] {
|
||||
return func(ctx context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
return result.Of(cfg1.value1 + len(cfg2.value2))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := Sequence(original)
|
||||
|
||||
outcome := sequenced(Config1{value1: 0})(Config2{value2: ""})(ctx)()
|
||||
assert.Equal(t, result.Of(0), outcome)
|
||||
})
|
||||
|
||||
t.Run("maintains referential transparency", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderReaderIOResult[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[ReaderReaderIOResult[Config1, int]] {
|
||||
return func() Result[ReaderReaderIOResult[Config1, int]] {
|
||||
return result.Of(func(cfg1 Config1) RIORES.ReaderIOResult[int] {
|
||||
return func(ctx context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
return result.Of(cfg1.value1 * len(cfg2.value2))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := Sequence(original)
|
||||
|
||||
cfg1 := Config1{value1: 3}
|
||||
cfg2 := Config2{value2: "test"}
|
||||
|
||||
// Call multiple times with same inputs
|
||||
for range 5 {
|
||||
outcome := sequenced(cfg1)(cfg2)(ctx)()
|
||||
assert.Equal(t, result.Of(12), outcome)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSequenceReader(t *testing.T) {
|
||||
t.Run("swaps parameter order for Reader types", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
// Original: takes Config2, returns ReaderIOResult that may produce Reader[Config1, int]
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[Reader[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[Reader[Config1, int]] {
|
||||
return func() Result[Reader[Config1, int]] {
|
||||
return result.Of(func(cfg1 Config1) int {
|
||||
return cfg1.value1 + len(cfg2.value2)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sequence swaps Config1 and Config2 order
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
cfg1 := Config1{value1: 10}
|
||||
cfg2 := Config2{value2: "hello"}
|
||||
|
||||
// Test original
|
||||
result1 := original(cfg2)(ctx)()
|
||||
assert.True(t, result.IsRight(result1))
|
||||
innerFunc1, _ := result.Unwrap(result1)
|
||||
value1 := innerFunc1(cfg1)
|
||||
assert.Equal(t, 15, value1)
|
||||
|
||||
// Test sequenced
|
||||
innerFunc2 := sequenced(cfg1)
|
||||
result2 := innerFunc2(cfg2)(ctx)()
|
||||
assert.True(t, result.IsRight(result2))
|
||||
value2, _ := result.Unwrap(result2)
|
||||
assert.Equal(t, 15, value2)
|
||||
})
|
||||
|
||||
t.Run("preserves error handling", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
testErr := errors.New("test error")
|
||||
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[Reader[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[Reader[Config1, int]] {
|
||||
return func() Result[Reader[Config1, int]] {
|
||||
return result.Left[Reader[Config1, int]](testErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
outcome := sequenced(Config1{value1: 10})(Config2{value2: "hello"})(ctx)()
|
||||
assert.Equal(t, result.Left[int](testErr), outcome)
|
||||
})
|
||||
|
||||
t.Run("works with pure Reader computations", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[Reader[Config1, string]] {
|
||||
return func(ctx context.Context) IOResult[Reader[Config1, string]] {
|
||||
return func() Result[Reader[Config1, string]] {
|
||||
if len(cfg2.value2) == 0 {
|
||||
return result.Left[Reader[Config1, string]](errors.New("empty string"))
|
||||
}
|
||||
return result.Of(func(cfg1 Config1) string {
|
||||
return fmt.Sprintf("%s:%d", cfg2.value2, cfg1.value1)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
// Test with valid inputs
|
||||
result1 := sequenced(Config1{value1: 42})(Config2{value2: "test"})(ctx)()
|
||||
assert.Equal(t, result.Of("test:42"), result1)
|
||||
|
||||
// Test with empty string
|
||||
result2 := sequenced(Config1{value1: 42})(Config2{value2: ""})(ctx)()
|
||||
assert.True(t, result.IsLeft(result2))
|
||||
})
|
||||
|
||||
t.Run("works with zero values", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[Reader[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[Reader[Config1, int]] {
|
||||
return func() Result[Reader[Config1, int]] {
|
||||
return result.Of(func(cfg1 Config1) int {
|
||||
return cfg1.value1 + len(cfg2.value2)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
outcome := sequenced(Config1{value1: 0})(Config2{value2: ""})(ctx)()
|
||||
assert.Equal(t, result.Of(0), outcome)
|
||||
})
|
||||
|
||||
t.Run("maintains referential transparency", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[Reader[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[Reader[Config1, int]] {
|
||||
return func() Result[Reader[Config1, int]] {
|
||||
return result.Of(func(cfg1 Config1) int {
|
||||
return cfg1.value1 * len(cfg2.value2)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
cfg1 := Config1{value1: 3}
|
||||
cfg2 := Config2{value2: "test"}
|
||||
|
||||
// Call multiple times with same inputs
|
||||
for range 5 {
|
||||
outcome := sequenced(cfg1)(cfg2)(ctx)()
|
||||
assert.Equal(t, result.Of(12), outcome)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSequenceReaderIO(t *testing.T) {
|
||||
t.Run("swaps parameter order for ReaderIO types", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
// Original: takes Config2, returns ReaderIOResult that may produce ReaderIO[Config1, int]
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderIO[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[ReaderIO[Config1, int]] {
|
||||
return func() Result[ReaderIO[Config1, int]] {
|
||||
return result.Of(func(cfg1 Config1) io.IO[int] {
|
||||
return io.Of(cfg1.value1 + len(cfg2.value2))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sequence swaps Config1 and Config2 order
|
||||
sequenced := SequenceReaderIO(original)
|
||||
|
||||
cfg1 := Config1{value1: 10}
|
||||
cfg2 := Config2{value2: "hello"}
|
||||
|
||||
// Test original
|
||||
result1 := original(cfg2)(ctx)()
|
||||
assert.True(t, result.IsRight(result1))
|
||||
innerFunc1, _ := result.Unwrap(result1)
|
||||
value1 := innerFunc1(cfg1)()
|
||||
assert.Equal(t, 15, value1)
|
||||
|
||||
// Test sequenced
|
||||
innerFunc2 := sequenced(cfg1)
|
||||
result2 := innerFunc2(cfg2)(ctx)()
|
||||
assert.True(t, result.IsRight(result2))
|
||||
value2, _ := result.Unwrap(result2)
|
||||
assert.Equal(t, 15, value2)
|
||||
})
|
||||
|
||||
t.Run("preserves error handling", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
testErr := errors.New("test error")
|
||||
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderIO[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[ReaderIO[Config1, int]] {
|
||||
return func() Result[ReaderIO[Config1, int]] {
|
||||
return result.Left[ReaderIO[Config1, int]](testErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := SequenceReaderIO(original)
|
||||
|
||||
outcome := sequenced(Config1{value1: 10})(Config2{value2: "hello"})(ctx)()
|
||||
assert.Equal(t, result.Left[int](testErr), outcome)
|
||||
})
|
||||
|
||||
t.Run("works with IO effects", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
sideEffect := 0
|
||||
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderIO[Config1, string]] {
|
||||
return func(ctx context.Context) IOResult[ReaderIO[Config1, string]] {
|
||||
return func() Result[ReaderIO[Config1, string]] {
|
||||
if len(cfg2.value2) == 0 {
|
||||
return result.Left[ReaderIO[Config1, string]](errors.New("empty string"))
|
||||
}
|
||||
return result.Of(func(cfg1 Config1) io.IO[string] {
|
||||
return func() string {
|
||||
sideEffect = cfg1.value1
|
||||
return fmt.Sprintf("%s:%d", cfg2.value2, cfg1.value1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := SequenceReaderIO(original)
|
||||
|
||||
// Test with valid inputs
|
||||
sideEffect = 0
|
||||
result1 := sequenced(Config1{value1: 42})(Config2{value2: "test"})(ctx)()
|
||||
assert.Equal(t, result.Of("test:42"), result1)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
|
||||
// Test with empty string
|
||||
sideEffect = 0
|
||||
result2 := sequenced(Config1{value1: 42})(Config2{value2: ""})(ctx)()
|
||||
assert.True(t, result.IsLeft(result2))
|
||||
assert.Equal(t, 0, sideEffect) // Side effect should not occur
|
||||
})
|
||||
|
||||
t.Run("works with zero values", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderIO[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[ReaderIO[Config1, int]] {
|
||||
return func() Result[ReaderIO[Config1, int]] {
|
||||
return result.Of(func(cfg1 Config1) io.IO[int] {
|
||||
return io.Of(cfg1.value1 + len(cfg2.value2))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := SequenceReaderIO(original)
|
||||
|
||||
outcome := sequenced(Config1{value1: 0})(Config2{value2: ""})(ctx)()
|
||||
assert.Equal(t, result.Of(0), outcome)
|
||||
})
|
||||
|
||||
t.Run("executes IO effects correctly", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
counter := 0
|
||||
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderIO[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[ReaderIO[Config1, int]] {
|
||||
return func() Result[ReaderIO[Config1, int]] {
|
||||
return result.Of(func(cfg1 Config1) io.IO[int] {
|
||||
return func() int {
|
||||
counter++
|
||||
return cfg1.value1 + len(cfg2.value2)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := SequenceReaderIO(original)
|
||||
|
||||
cfg1 := Config1{value1: 10}
|
||||
cfg2 := Config2{value2: "hello"}
|
||||
|
||||
// Each execution should increment counter
|
||||
counter = 0
|
||||
result1 := sequenced(cfg1)(cfg2)(ctx)()
|
||||
assert.Equal(t, result.Of(15), result1)
|
||||
assert.Equal(t, 1, counter)
|
||||
|
||||
result2 := sequenced(cfg1)(cfg2)(ctx)()
|
||||
assert.Equal(t, result.Of(15), result2)
|
||||
assert.Equal(t, 2, counter)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTraverse(t *testing.T) {
|
||||
t.Run("transforms and swaps parameter order", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
// Original computation depending on Config2
|
||||
original := Of[Config2](42)
|
||||
|
||||
// Transformation that introduces Config1 dependency
|
||||
transform := func(n int) ReaderReaderIOResult[Config1, string] {
|
||||
return func(cfg1 Config1) RIORES.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
return result.Of(fmt.Sprintf("value=%d, cfg1=%d", n, cfg1.value1))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply traverse to swap order and transform
|
||||
traversed := Traverse[Config2](transform)(original)
|
||||
|
||||
cfg1 := Config1{value1: 100}
|
||||
cfg2 := Config2{value2: "test"}
|
||||
|
||||
outcome := traversed(cfg1)(cfg2)(ctx)()
|
||||
assert.Equal(t, result.Of("value=42, cfg1=100"), outcome)
|
||||
})
|
||||
|
||||
t.Run("preserves error handling in original", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
testErr := errors.New("test error")
|
||||
original := Left[Config2, int](testErr)
|
||||
|
||||
transform := func(n int) ReaderReaderIOResult[Config1, string] {
|
||||
return Of[Config1](fmt.Sprintf("%d", n))
|
||||
}
|
||||
|
||||
traversed := Traverse[Config2](transform)(original)
|
||||
|
||||
outcome := traversed(Config1{value1: 100})(Config2{value2: "test"})(ctx)()
|
||||
assert.Equal(t, result.Left[string](testErr), outcome)
|
||||
})
|
||||
|
||||
t.Run("preserves error handling in transformation", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
original := Of[Config2](42)
|
||||
testErr := errors.New("transform error")
|
||||
|
||||
transform := func(n int) ReaderReaderIOResult[Config1, string] {
|
||||
if n < 0 {
|
||||
return Left[Config1, string](testErr)
|
||||
}
|
||||
return Of[Config1](fmt.Sprintf("%d", n))
|
||||
}
|
||||
|
||||
// Test with negative value
|
||||
originalNeg := Of[Config2](-1)
|
||||
traversedNeg := Traverse[Config2](transform)(originalNeg)
|
||||
resultNeg := traversedNeg(Config1{value1: 100})(Config2{value2: "test"})(ctx)()
|
||||
assert.Equal(t, result.Left[string](testErr), resultNeg)
|
||||
|
||||
// Test with positive value
|
||||
traversedPos := Traverse[Config2](transform)(original)
|
||||
resultPos := traversedPos(Config1{value1: 100})(Config2{value2: "test"})(ctx)()
|
||||
assert.Equal(t, result.Of("42"), resultPos)
|
||||
})
|
||||
|
||||
t.Run("works with complex transformations", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
original := Of[Config2](10)
|
||||
|
||||
transform := func(n int) ReaderReaderIOResult[Config1, int] {
|
||||
return func(cfg1 Config1) RIORES.ReaderIOResult[int] {
|
||||
return func(ctx context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
return result.Of(n * cfg1.value1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traversed := Traverse[Config2](transform)(original)
|
||||
|
||||
outcome := traversed(Config1{value1: 5})(Config2{value2: "test"})(ctx)()
|
||||
assert.Equal(t, result.Of(50), outcome)
|
||||
})
|
||||
|
||||
t.Run("can be composed with other operations", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
original := Of[Config2](10)
|
||||
|
||||
transform := func(n int) ReaderReaderIOResult[Config1, int] {
|
||||
return Of[Config1](n * 2)
|
||||
}
|
||||
|
||||
outcome := F.Pipe2(
|
||||
original,
|
||||
Traverse[Config2](transform),
|
||||
func(k Kleisli[Config2, Config1, int]) ReaderReaderIOResult[Config2, int] {
|
||||
return k(Config1{value1: 5})
|
||||
},
|
||||
)
|
||||
|
||||
res := outcome(Config2{value2: "test"})(ctx)()
|
||||
assert.Equal(t, result.Of(20), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTraverseReader(t *testing.T) {
|
||||
t.Run("transforms with pure Reader and swaps parameter order", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
// Original computation depending on Config2
|
||||
original := Of[Config2](100)
|
||||
|
||||
// Pure Reader transformation that introduces Config1 dependency
|
||||
formatWithConfig := func(value int) reader.Reader[Config1, string] {
|
||||
return func(cfg1 Config1) string {
|
||||
return fmt.Sprintf("value=%d, multiplier=%d, result=%d", value, cfg1.value1, value*cfg1.value1)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply traverse to introduce Config1 and swap order
|
||||
traversed := TraverseReader[Config2](formatWithConfig)(original)
|
||||
|
||||
cfg1 := Config1{value1: 5}
|
||||
cfg2 := Config2{value2: "test"}
|
||||
|
||||
outcome := traversed(cfg1)(cfg2)(ctx)()
|
||||
assert.Equal(t, result.Of("value=100, multiplier=5, result=500"), outcome)
|
||||
})
|
||||
|
||||
t.Run("preserves error handling", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
testErr := errors.New("test error")
|
||||
original := Left[Config2, int](testErr)
|
||||
|
||||
transform := func(n int) reader.Reader[Config1, string] {
|
||||
return reader.Of[Config1](fmt.Sprintf("%d", n))
|
||||
}
|
||||
|
||||
traversed := TraverseReader[Config2](transform)(original)
|
||||
|
||||
outcome := traversed(Config1{value1: 5})(Config2{value2: "test"})(ctx)()
|
||||
assert.Equal(t, result.Left[string](testErr), outcome)
|
||||
})
|
||||
|
||||
t.Run("works with pure computations", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
original := Of[Config2](42)
|
||||
|
||||
// Pure transformation using Reader
|
||||
double := func(n int) reader.Reader[Config1, int] {
|
||||
return func(cfg1 Config1) int {
|
||||
return n * cfg1.value1
|
||||
}
|
||||
}
|
||||
|
||||
traversed := TraverseReader[Config2](double)(original)
|
||||
|
||||
outcome := traversed(Config1{value1: 3})(Config2{value2: "test"})(ctx)()
|
||||
assert.Equal(t, result.Of(126), outcome)
|
||||
})
|
||||
|
||||
t.Run("works with zero values", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
original := Of[Config2](0)
|
||||
|
||||
transform := func(n int) reader.Reader[Config1, int] {
|
||||
return func(cfg1 Config1) int {
|
||||
return n + cfg1.value1
|
||||
}
|
||||
}
|
||||
|
||||
traversed := TraverseReader[Config2](transform)(original)
|
||||
|
||||
outcome := traversed(Config1{value1: 0})(Config2{value2: ""})(ctx)()
|
||||
assert.Equal(t, result.Of(0), outcome)
|
||||
})
|
||||
|
||||
t.Run("maintains referential transparency", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
original := Of[Config2](10)
|
||||
|
||||
transform := func(n int) reader.Reader[Config1, int] {
|
||||
return func(cfg1 Config1) int {
|
||||
return n * cfg1.value1
|
||||
}
|
||||
}
|
||||
|
||||
traversed := TraverseReader[Config2](transform)(original)
|
||||
|
||||
cfg1 := Config1{value1: 5}
|
||||
cfg2 := Config2{value2: "test"}
|
||||
|
||||
// Call multiple times with same inputs
|
||||
for range 5 {
|
||||
outcome := traversed(cfg1)(cfg2)(ctx)()
|
||||
assert.Equal(t, result.Of(50), outcome)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("can be used in composition", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
original := Of[Config2](10)
|
||||
|
||||
multiply := func(n int) reader.Reader[Config1, int] {
|
||||
return func(cfg1 Config1) int {
|
||||
return n * cfg1.value1
|
||||
}
|
||||
}
|
||||
|
||||
outcome := F.Pipe2(
|
||||
original,
|
||||
TraverseReader[Config2](multiply),
|
||||
func(k Kleisli[Config2, Config1, int]) ReaderReaderIOResult[Config2, int] {
|
||||
return k(Config1{value1: 3})
|
||||
},
|
||||
)
|
||||
|
||||
res := outcome(Config2{value2: "test"})(ctx)()
|
||||
assert.Equal(t, result.Of(30), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFlipIntegration(t *testing.T) {
|
||||
t.Run("Sequence and Traverse work together", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
// Create a nested computation
|
||||
nested := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderReaderIOResult[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[ReaderReaderIOResult[Config1, int]] {
|
||||
return func() Result[ReaderReaderIOResult[Config1, int]] {
|
||||
return result.Of(Of[Config1](len(cfg2.value2)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sequence it
|
||||
sequenced := Sequence(nested)
|
||||
|
||||
// Then traverse with a transformation
|
||||
transform := func(n int) ReaderReaderIOResult[Config1, string] {
|
||||
return Of[Config1](fmt.Sprintf("length=%d", n))
|
||||
}
|
||||
|
||||
// Apply both operations
|
||||
cfg1 := Config1{value1: 10}
|
||||
cfg2 := Config2{value2: "hello"}
|
||||
|
||||
// First sequence
|
||||
intermediate := sequenced(cfg1)(cfg2)(ctx)()
|
||||
assert.Equal(t, result.Of(5), intermediate)
|
||||
|
||||
// Then apply traverse on a new computation
|
||||
original := Of[Config2](5)
|
||||
traversed := Traverse[Config2](transform)(original)
|
||||
outcome := traversed(cfg1)(cfg2)(ctx)()
|
||||
assert.Equal(t, result.Of("length=5"), outcome)
|
||||
})
|
||||
|
||||
t.Run("all flip functions preserve error semantics", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
testErr := errors.New("test error")
|
||||
cfg1 := Config1{value1: 10}
|
||||
cfg2 := Config2{value2: "test"}
|
||||
|
||||
// Test Sequence with error
|
||||
seqErr := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderReaderIOResult[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[ReaderReaderIOResult[Config1, int]] {
|
||||
return func() Result[ReaderReaderIOResult[Config1, int]] {
|
||||
return result.Left[ReaderReaderIOResult[Config1, int]](testErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
seqResult := Sequence(seqErr)(cfg1)(cfg2)(ctx)()
|
||||
assert.True(t, result.IsLeft(seqResult))
|
||||
|
||||
// Test SequenceReader with error
|
||||
seqReaderErr := func(cfg2 Config2) RIORES.ReaderIOResult[Reader[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[Reader[Config1, int]] {
|
||||
return func() Result[Reader[Config1, int]] {
|
||||
return result.Left[Reader[Config1, int]](testErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
seqReaderResult := SequenceReader(seqReaderErr)(cfg1)(cfg2)(ctx)()
|
||||
assert.True(t, result.IsLeft(seqReaderResult))
|
||||
|
||||
// Test SequenceReaderIO with error
|
||||
seqReaderIOErr := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderIO[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[ReaderIO[Config1, int]] {
|
||||
return func() Result[ReaderIO[Config1, int]] {
|
||||
return result.Left[ReaderIO[Config1, int]](testErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
seqReaderIOResult := SequenceReaderIO(seqReaderIOErr)(cfg1)(cfg2)(ctx)()
|
||||
assert.True(t, result.IsLeft(seqReaderIOResult))
|
||||
|
||||
// Test Traverse with error
|
||||
travErr := Left[Config2, int](testErr)
|
||||
travTransform := func(n int) ReaderReaderIOResult[Config1, string] {
|
||||
return Of[Config1](fmt.Sprintf("%d", n))
|
||||
}
|
||||
travResult := Traverse[Config2](travTransform)(travErr)(cfg1)(cfg2)(ctx)()
|
||||
assert.True(t, result.IsLeft(travResult))
|
||||
|
||||
// Test TraverseReader with error
|
||||
travReaderErr := Left[Config2, int](testErr)
|
||||
travReaderTransform := func(n int) reader.Reader[Config1, string] {
|
||||
return reader.Of[Config1](fmt.Sprintf("%d", n))
|
||||
}
|
||||
travReaderResult := TraverseReader[Config2](travReaderTransform)(travReaderErr)(cfg1)(cfg2)(ctx)()
|
||||
assert.True(t, result.IsLeft(travReaderResult))
|
||||
})
|
||||
}
|
||||
148
v2/context/readerreaderioresult/monoid.go
Normal file
148
v2/context/readerreaderioresult/monoid.go
Normal file
@@ -0,0 +1,148 @@
|
||||
// Copyright (c) 2023 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerreaderioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
)
|
||||
|
||||
type (
|
||||
// Monoid represents a monoid structure for ReaderReaderIOResult[R, A].
|
||||
// A monoid provides an identity element (empty) and an associative binary operation (concat).
|
||||
Monoid[R, A any] = monoid.Monoid[ReaderReaderIOResult[R, A]]
|
||||
)
|
||||
|
||||
// ApplicativeMonoid creates a monoid for ReaderReaderIOResult using applicative composition.
|
||||
// It combines values using the provided monoid m and the applicative Ap operation.
|
||||
// This allows combining multiple ReaderReaderIOResult values in parallel while merging their results.
|
||||
//
|
||||
// The resulting monoid satisfies:
|
||||
// - Identity: concat(empty, x) = concat(x, empty) = x
|
||||
// - Associativity: concat(concat(x, y), z) = concat(x, concat(y, z))
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import "github.com/IBM/fp-go/v2/monoid"
|
||||
// import "github.com/IBM/fp-go/v2/number"
|
||||
//
|
||||
// // Create a monoid for combining integers with addition
|
||||
// intMonoid := ApplicativeMonoid[Config](number.MonoidSum)
|
||||
//
|
||||
// // Combine multiple computations
|
||||
// result := intMonoid.Concat(
|
||||
// Of[Config](10),
|
||||
// intMonoid.Concat(Of[Config](20), Of[Config](30)),
|
||||
// ) // Results in 60
|
||||
func ApplicativeMonoid[R, A any](m monoid.Monoid[A]) Monoid[R, A] {
|
||||
return monoid.ApplicativeMonoid(
|
||||
Of[R, A],
|
||||
MonadMap[R, A, func(A) A],
|
||||
MonadAp[R, A, A],
|
||||
m,
|
||||
)
|
||||
}
|
||||
|
||||
// ApplicativeMonoidSeq creates a monoid for ReaderReaderIOResult using sequential applicative composition.
|
||||
// Similar to ApplicativeMonoid but evaluates effects sequentially rather than in parallel.
|
||||
//
|
||||
// Use this when:
|
||||
// - Effects must be executed in a specific order
|
||||
// - Side effects depend on sequential execution
|
||||
// - You want to avoid concurrent execution
|
||||
func ApplicativeMonoidSeq[R, A any](m monoid.Monoid[A]) Monoid[R, A] {
|
||||
return monoid.ApplicativeMonoid(
|
||||
Of[R, A],
|
||||
MonadMap[R, A, func(A) A],
|
||||
MonadApSeq[R, A, A],
|
||||
m,
|
||||
)
|
||||
}
|
||||
|
||||
// ApplicativeMonoidPar creates a monoid for ReaderReaderIOResult using parallel applicative composition.
|
||||
// Similar to ApplicativeMonoid but explicitly evaluates effects in parallel.
|
||||
//
|
||||
// Use this when:
|
||||
// - Effects are independent and can run concurrently
|
||||
// - You want to maximize performance through parallelism
|
||||
// - Order of execution doesn't matter
|
||||
func ApplicativeMonoidPar[R, A any](m monoid.Monoid[A]) Monoid[R, A] {
|
||||
return monoid.ApplicativeMonoid(
|
||||
Of[R, A],
|
||||
MonadMap[R, A, func(A) A],
|
||||
MonadApPar[R, A, A],
|
||||
m,
|
||||
)
|
||||
}
|
||||
|
||||
// AlternativeMonoid creates a monoid that combines ReaderReaderIOResult values using both
|
||||
// applicative composition and alternative (Alt) semantics.
|
||||
//
|
||||
// This monoid:
|
||||
// - Uses Ap for combining successful values
|
||||
// - Uses Alt for handling failures (tries alternatives on failure)
|
||||
// - Provides a way to combine multiple computations with fallback behavior
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import "github.com/IBM/fp-go/v2/monoid"
|
||||
// import "github.com/IBM/fp-go/v2/number"
|
||||
//
|
||||
// intMonoid := AlternativeMonoid[Config](number.MonoidSum)
|
||||
//
|
||||
// // If first computation fails, tries the second
|
||||
// result := intMonoid.Concat(
|
||||
// Left[Config, int](errors.New("failed")),
|
||||
// Of[Config](42),
|
||||
// ) // Results in Right(42)
|
||||
func AlternativeMonoid[R, A any](m monoid.Monoid[A]) Monoid[R, A] {
|
||||
return monoid.AlternativeMonoid(
|
||||
Of[R, A],
|
||||
MonadMap[R, A, func(A) A],
|
||||
MonadAp[R, A, A],
|
||||
MonadAlt[R, A],
|
||||
m,
|
||||
)
|
||||
}
|
||||
|
||||
// AltMonoid creates a monoid based solely on the Alt operation.
|
||||
// It provides a way to chain computations with fallback behavior.
|
||||
//
|
||||
// The monoid:
|
||||
// - Uses the provided zero as the identity element
|
||||
// - Uses Alt for concatenation (tries first, falls back to second on failure)
|
||||
// - Implements a "first success" strategy
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// zero := func() ReaderReaderIOResult[Config, int] {
|
||||
// return Left[Config, int](errors.New("no value"))
|
||||
// }
|
||||
// altMonoid := AltMonoid[Config, int](zero)
|
||||
//
|
||||
// // Tries computations in order until one succeeds
|
||||
// result := altMonoid.Concat(
|
||||
// Left[Config, int](errors.New("first failed")),
|
||||
// altMonoid.Concat(
|
||||
// Left[Config, int](errors.New("second failed")),
|
||||
// Of[Config](42),
|
||||
// ),
|
||||
// ) // Results in Right(42)
|
||||
func AltMonoid[R, A any](zero Lazy[ReaderReaderIOResult[R, A]]) Monoid[R, A] {
|
||||
return monoid.AltMonoid(
|
||||
zero,
|
||||
MonadAlt[R, A],
|
||||
)
|
||||
}
|
||||
337
v2/context/readerreaderioresult/monoid_test.go
Normal file
337
v2/context/readerreaderioresult/monoid_test.go
Normal file
@@ -0,0 +1,337 @@
|
||||
// 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 readerreaderioresult
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
intAddMonoid = N.MonoidSum[int]()
|
||||
strMonoid = S.Monoid
|
||||
testError = errors.New("test error")
|
||||
)
|
||||
|
||||
func TestApplicativeMonoid(t *testing.T) {
|
||||
rrMonoid := ApplicativeMonoid[AppConfig](intAddMonoid)
|
||||
cfg := defaultConfig
|
||||
ctx := t.Context()
|
||||
|
||||
t.Run("empty element", func(t *testing.T) {
|
||||
empty := rrMonoid.Empty()
|
||||
assert.Equal(t, result.Of(0), empty(cfg)(ctx)())
|
||||
})
|
||||
|
||||
t.Run("concat two success values", func(t *testing.T) {
|
||||
rr1 := Of[AppConfig](5)
|
||||
rr2 := Of[AppConfig](3)
|
||||
combined := rrMonoid.Concat(rr1, rr2)
|
||||
assert.Equal(t, result.Of(8), combined(cfg)(ctx)())
|
||||
})
|
||||
|
||||
t.Run("concat with empty", func(t *testing.T) {
|
||||
rr := Of[AppConfig](42)
|
||||
combined1 := rrMonoid.Concat(rr, rrMonoid.Empty())
|
||||
combined2 := rrMonoid.Concat(rrMonoid.Empty(), rr)
|
||||
|
||||
assert.Equal(t, result.Of(42), combined1(cfg)(ctx)())
|
||||
assert.Equal(t, result.Of(42), combined2(cfg)(ctx)())
|
||||
})
|
||||
|
||||
t.Run("concat with left failure", func(t *testing.T) {
|
||||
rrSuccess := Of[AppConfig](5)
|
||||
rrFailure := Left[AppConfig, int](testError)
|
||||
|
||||
combined := rrMonoid.Concat(rrFailure, rrSuccess)
|
||||
assert.True(t, result.IsLeft(combined(cfg)(ctx)()))
|
||||
})
|
||||
|
||||
t.Run("concat with right failure", func(t *testing.T) {
|
||||
rrSuccess := Of[AppConfig](5)
|
||||
rrFailure := Left[AppConfig, int](testError)
|
||||
|
||||
combined := rrMonoid.Concat(rrSuccess, rrFailure)
|
||||
assert.True(t, result.IsLeft(combined(cfg)(ctx)()))
|
||||
})
|
||||
|
||||
t.Run("concat multiple values", func(t *testing.T) {
|
||||
rr1 := Of[AppConfig](1)
|
||||
rr2 := Of[AppConfig](2)
|
||||
rr3 := Of[AppConfig](3)
|
||||
rr4 := Of[AppConfig](4)
|
||||
|
||||
// Chain concat calls: ((1 + 2) + 3) + 4
|
||||
combined := rrMonoid.Concat(
|
||||
rrMonoid.Concat(
|
||||
rrMonoid.Concat(rr1, rr2),
|
||||
rr3,
|
||||
),
|
||||
rr4,
|
||||
)
|
||||
assert.Equal(t, result.Of(10), combined(cfg)(ctx)())
|
||||
})
|
||||
|
||||
t.Run("string concatenation", func(t *testing.T) {
|
||||
strRRMonoid := ApplicativeMonoid[AppConfig](strMonoid)
|
||||
|
||||
rr1 := Of[AppConfig]("Hello")
|
||||
rr2 := Of[AppConfig](" ")
|
||||
rr3 := Of[AppConfig]("World")
|
||||
|
||||
combined := strRRMonoid.Concat(
|
||||
strRRMonoid.Concat(rr1, rr2),
|
||||
rr3,
|
||||
)
|
||||
assert.Equal(t, result.Of("Hello World"), combined(cfg)(ctx)())
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeMonoidSeq(t *testing.T) {
|
||||
rrMonoid := ApplicativeMonoidSeq[AppConfig](intAddMonoid)
|
||||
cfg := defaultConfig
|
||||
ctx := t.Context()
|
||||
|
||||
t.Run("empty element", func(t *testing.T) {
|
||||
empty := rrMonoid.Empty()
|
||||
assert.Equal(t, result.Of(0), empty(cfg)(ctx)())
|
||||
})
|
||||
|
||||
t.Run("concat two success values", func(t *testing.T) {
|
||||
rr1 := Of[AppConfig](5)
|
||||
rr2 := Of[AppConfig](3)
|
||||
combined := rrMonoid.Concat(rr1, rr2)
|
||||
assert.Equal(t, result.Of(8), combined(cfg)(ctx)())
|
||||
})
|
||||
|
||||
t.Run("concat with failure", func(t *testing.T) {
|
||||
rrSuccess := Of[AppConfig](5)
|
||||
rrFailure := Left[AppConfig, int](testError)
|
||||
|
||||
combined := rrMonoid.Concat(rrFailure, rrSuccess)
|
||||
assert.True(t, result.IsLeft(combined(cfg)(ctx)()))
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeMonoidPar(t *testing.T) {
|
||||
rrMonoid := ApplicativeMonoidPar[AppConfig](intAddMonoid)
|
||||
cfg := defaultConfig
|
||||
ctx := t.Context()
|
||||
|
||||
t.Run("empty element", func(t *testing.T) {
|
||||
empty := rrMonoid.Empty()
|
||||
assert.Equal(t, result.Of(0), empty(cfg)(ctx)())
|
||||
})
|
||||
|
||||
t.Run("concat two success values", func(t *testing.T) {
|
||||
rr1 := Of[AppConfig](5)
|
||||
rr2 := Of[AppConfig](3)
|
||||
combined := rrMonoid.Concat(rr1, rr2)
|
||||
assert.Equal(t, result.Of(8), combined(cfg)(ctx)())
|
||||
})
|
||||
|
||||
t.Run("concat with failure", func(t *testing.T) {
|
||||
rrSuccess := Of[AppConfig](5)
|
||||
rrFailure := Left[AppConfig, int](testError)
|
||||
|
||||
combined := rrMonoid.Concat(rrFailure, rrSuccess)
|
||||
assert.True(t, result.IsLeft(combined(cfg)(ctx)()))
|
||||
})
|
||||
}
|
||||
|
||||
func TestAltMonoid(t *testing.T) {
|
||||
zero := func() ReaderReaderIOResult[AppConfig, int] {
|
||||
return Left[AppConfig, int](errors.New("empty"))
|
||||
}
|
||||
|
||||
rrMonoid := AltMonoid(zero)
|
||||
cfg := defaultConfig
|
||||
ctx := t.Context()
|
||||
|
||||
t.Run("empty element", func(t *testing.T) {
|
||||
empty := rrMonoid.Empty()
|
||||
assert.True(t, result.IsLeft(empty(cfg)(ctx)()))
|
||||
})
|
||||
|
||||
t.Run("concat two success values - uses first", func(t *testing.T) {
|
||||
rr1 := Of[AppConfig](5)
|
||||
rr2 := Of[AppConfig](3)
|
||||
combined := rrMonoid.Concat(rr1, rr2)
|
||||
// AltMonoid takes the first successful value
|
||||
assert.Equal(t, result.Of(5), combined(cfg)(ctx)())
|
||||
})
|
||||
|
||||
t.Run("concat failure then success", func(t *testing.T) {
|
||||
rrFailure := Left[AppConfig, int](testError)
|
||||
rrSuccess := Of[AppConfig](42)
|
||||
|
||||
combined := rrMonoid.Concat(rrFailure, rrSuccess)
|
||||
// Should fall back to second when first fails
|
||||
assert.Equal(t, result.Of(42), combined(cfg)(ctx)())
|
||||
})
|
||||
|
||||
t.Run("concat success then failure", func(t *testing.T) {
|
||||
rrSuccess := Of[AppConfig](42)
|
||||
rrFailure := Left[AppConfig, int](testError)
|
||||
|
||||
combined := rrMonoid.Concat(rrSuccess, rrFailure)
|
||||
// Should use first successful value
|
||||
assert.Equal(t, result.Of(42), combined(cfg)(ctx)())
|
||||
})
|
||||
|
||||
t.Run("concat two failures", func(t *testing.T) {
|
||||
err1 := errors.New("error 1")
|
||||
err2 := errors.New("error 2")
|
||||
|
||||
rr1 := Left[AppConfig, int](err1)
|
||||
rr2 := Left[AppConfig, int](err2)
|
||||
|
||||
combined := rrMonoid.Concat(rr1, rr2)
|
||||
// Should use second error when both fail
|
||||
assert.True(t, result.IsLeft(combined(cfg)(ctx)()))
|
||||
})
|
||||
|
||||
t.Run("concat with empty", func(t *testing.T) {
|
||||
rr := Of[AppConfig](42)
|
||||
combined1 := rrMonoid.Concat(rr, rrMonoid.Empty())
|
||||
combined2 := rrMonoid.Concat(rrMonoid.Empty(), rr)
|
||||
|
||||
assert.Equal(t, result.Of(42), combined1(cfg)(ctx)())
|
||||
assert.Equal(t, result.Of(42), combined2(cfg)(ctx)())
|
||||
})
|
||||
|
||||
t.Run("fallback chain", func(t *testing.T) {
|
||||
// Simulate trying multiple sources until one succeeds
|
||||
primary := Left[AppConfig, string](errors.New("primary failed"))
|
||||
secondary := Left[AppConfig, string](errors.New("secondary failed"))
|
||||
tertiary := Of[AppConfig]("tertiary success")
|
||||
|
||||
strZero := func() ReaderReaderIOResult[AppConfig, string] {
|
||||
return Left[AppConfig, string](errors.New("all failed"))
|
||||
}
|
||||
strMonoid := AltMonoid(strZero)
|
||||
|
||||
// Chain concat: try primary, then secondary, then tertiary
|
||||
combined := strMonoid.Concat(
|
||||
strMonoid.Concat(primary, secondary),
|
||||
tertiary,
|
||||
)
|
||||
assert.Equal(t, result.Of("tertiary success"), combined(cfg)(ctx)())
|
||||
})
|
||||
}
|
||||
|
||||
func TestAlternativeMonoid(t *testing.T) {
|
||||
rrMonoid := AlternativeMonoid[AppConfig](intAddMonoid)
|
||||
cfg := defaultConfig
|
||||
ctx := t.Context()
|
||||
|
||||
t.Run("empty element", func(t *testing.T) {
|
||||
empty := rrMonoid.Empty()
|
||||
assert.Equal(t, result.Of(0), empty(cfg)(ctx)())
|
||||
})
|
||||
|
||||
t.Run("concat two success values", func(t *testing.T) {
|
||||
rr1 := Of[AppConfig](5)
|
||||
rr2 := Of[AppConfig](3)
|
||||
combined := rrMonoid.Concat(rr1, rr2)
|
||||
assert.Equal(t, result.Of(8), combined(cfg)(ctx)())
|
||||
})
|
||||
|
||||
t.Run("concat failure then success", func(t *testing.T) {
|
||||
rrFailure := Left[AppConfig, int](testError)
|
||||
rrSuccess := Of[AppConfig](42)
|
||||
|
||||
combined := rrMonoid.Concat(rrFailure, rrSuccess)
|
||||
// Alternative falls back to second when first fails
|
||||
assert.Equal(t, result.Of(42), combined(cfg)(ctx)())
|
||||
})
|
||||
|
||||
t.Run("concat success then failure", func(t *testing.T) {
|
||||
rrSuccess := Of[AppConfig](42)
|
||||
rrFailure := Left[AppConfig, int](testError)
|
||||
|
||||
combined := rrMonoid.Concat(rrSuccess, rrFailure)
|
||||
// Should use first successful value
|
||||
assert.Equal(t, result.Of(42), combined(cfg)(ctx)())
|
||||
})
|
||||
|
||||
t.Run("concat with empty", func(t *testing.T) {
|
||||
rr := Of[AppConfig](42)
|
||||
combined1 := rrMonoid.Concat(rr, rrMonoid.Empty())
|
||||
combined2 := rrMonoid.Concat(rrMonoid.Empty(), rr)
|
||||
|
||||
assert.Equal(t, result.Of(42), combined1(cfg)(ctx)())
|
||||
assert.Equal(t, result.Of(42), combined2(cfg)(ctx)())
|
||||
})
|
||||
|
||||
t.Run("multiple values with some failures", func(t *testing.T) {
|
||||
rr1 := Left[AppConfig, int](errors.New("fail 1"))
|
||||
rr2 := Of[AppConfig](5)
|
||||
rr3 := Left[AppConfig, int](errors.New("fail 2"))
|
||||
rr4 := Of[AppConfig](10)
|
||||
|
||||
// Alternative should skip failures and accumulate successes
|
||||
combined := rrMonoid.Concat(
|
||||
rrMonoid.Concat(
|
||||
rrMonoid.Concat(rr1, rr2),
|
||||
rr3,
|
||||
),
|
||||
rr4,
|
||||
)
|
||||
// Should accumulate successful values: 5 + 10 = 15
|
||||
assert.Equal(t, result.Of(15), combined(cfg)(ctx)())
|
||||
})
|
||||
}
|
||||
|
||||
// Test monoid laws
|
||||
func TestMonoidLaws(t *testing.T) {
|
||||
rrMonoid := ApplicativeMonoid[AppConfig](intAddMonoid)
|
||||
cfg := defaultConfig
|
||||
ctx := t.Context()
|
||||
|
||||
// Left identity: empty <> x == x
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
x := Of[AppConfig](42)
|
||||
result1 := rrMonoid.Concat(rrMonoid.Empty(), x)(cfg)(ctx)()
|
||||
result2 := x(cfg)(ctx)()
|
||||
assert.Equal(t, result2, result1)
|
||||
})
|
||||
|
||||
// Right identity: x <> empty == x
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
x := Of[AppConfig](42)
|
||||
result1 := rrMonoid.Concat(x, rrMonoid.Empty())(cfg)(ctx)()
|
||||
result2 := x(cfg)(ctx)()
|
||||
assert.Equal(t, result2, result1)
|
||||
})
|
||||
|
||||
// Associativity: (x <> y) <> z == x <> (y <> z)
|
||||
t.Run("associativity", func(t *testing.T) {
|
||||
x := Of[AppConfig](1)
|
||||
y := Of[AppConfig](2)
|
||||
z := Of[AppConfig](3)
|
||||
|
||||
left := rrMonoid.Concat(rrMonoid.Concat(x, y), z)(cfg)(ctx)()
|
||||
right := rrMonoid.Concat(x, rrMonoid.Concat(y, z))(cfg)(ctx)()
|
||||
|
||||
assert.Equal(t, right, left)
|
||||
})
|
||||
}
|
||||
894
v2/context/readerreaderioresult/reader.go
Normal file
894
v2/context/readerreaderioresult/reader.go
Normal file
@@ -0,0 +1,894 @@
|
||||
// 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 readerreaderioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/chain"
|
||||
"github.com/IBM/fp-go/v2/internal/fromeither"
|
||||
"github.com/IBM/fp-go/v2/internal/fromio"
|
||||
"github.com/IBM/fp-go/v2/internal/fromioeither"
|
||||
"github.com/IBM/fp-go/v2/internal/fromreader"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/readert"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
IOE "github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
RE "github.com/IBM/fp-go/v2/readereither"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
"github.com/IBM/fp-go/v2/readeroption"
|
||||
RRIOE "github.com/IBM/fp-go/v2/readerreaderioeither"
|
||||
)
|
||||
|
||||
// FromReaderOption converts a ReaderOption to a ReaderReaderIOResult.
|
||||
// If the option is None, it uses the provided onNone function to generate an error.
|
||||
//
|
||||
//go:inline
|
||||
func FromReaderOption[R, A any](onNone Lazy[error]) Kleisli[R, ReaderOption[R, A], A] {
|
||||
return RRIOE.FromReaderOption[R, context.Context, A](onNone)
|
||||
}
|
||||
|
||||
// FromReaderIOResult lifts a ReaderIOResult into a ReaderReaderIOResult.
|
||||
// This adds an additional reader layer to the computation.
|
||||
//
|
||||
//go:inline
|
||||
func FromReaderIOResult[R, A any](ma ReaderIOResult[R, A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.FromReaderIOEither[context.Context](ma)
|
||||
}
|
||||
|
||||
// FromReaderIO lifts a ReaderIO into a ReaderReaderIOResult.
|
||||
// The IO computation is wrapped in a Right (success) value.
|
||||
//
|
||||
//go:inline
|
||||
func FromReaderIO[R, A any](ma ReaderIO[R, A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.FromReaderIO[context.Context, error](ma)
|
||||
}
|
||||
|
||||
// RightReaderIO lifts a ReaderIO into a ReaderReaderIOResult as a Right (success) value.
|
||||
// Alias for FromReaderIO.
|
||||
//
|
||||
//go:inline
|
||||
func RightReaderIO[R, A any](ma ReaderIO[R, A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.RightReaderIO[context.Context, error](ma)
|
||||
}
|
||||
|
||||
// LeftReaderIO lifts a ReaderIO that produces an error into a ReaderReaderIOResult as a Left (failure) value.
|
||||
//
|
||||
//go:inline
|
||||
func LeftReaderIO[A, R any](me ReaderIO[R, error]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.LeftReaderIO[context.Context, A](me)
|
||||
}
|
||||
|
||||
// MonadMap applies a function to the value inside a ReaderReaderIOResult (Functor operation).
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadMap[R, A, B any](fa ReaderReaderIOResult[R, A], f func(A) B) ReaderReaderIOResult[R, B] {
|
||||
return reader.MonadMap(fa, RIOE.Map(f))
|
||||
}
|
||||
|
||||
// Map applies a function to the value inside a ReaderReaderIOResult (Functor operation).
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func Map[R, A, B any](f func(A) B) Operator[R, A, B] {
|
||||
return reader.Map[R](RIOE.Map(f))
|
||||
}
|
||||
|
||||
// MonadMapTo replaces the value inside a ReaderReaderIOResult with a constant value.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadMapTo[R, A, B any](fa ReaderReaderIOResult[R, A], b B) ReaderReaderIOResult[R, B] {
|
||||
return reader.MonadMap(fa, RIOE.MapTo[A](b))
|
||||
}
|
||||
|
||||
// MapTo replaces the value inside a ReaderReaderIOResult with a constant value.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func MapTo[R, A, B any](b B) Operator[R, A, B] {
|
||||
return reader.Map[R](RIOE.MapTo[A](b))
|
||||
}
|
||||
|
||||
// MonadChain sequences two computations, where the second depends on the result of the first (Monad operation).
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChain[R, A, B any](fa ReaderReaderIOResult[R, A], f Kleisli[R, A, B]) ReaderReaderIOResult[R, B] {
|
||||
return readert.MonadChain(
|
||||
RIOE.MonadChain[A, B],
|
||||
fa,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadChainFirst sequences two computations but returns the result of the first.
|
||||
// Useful for performing side effects while preserving the original value.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirst[R, A, B any](fa ReaderReaderIOResult[R, A], f Kleisli[R, A, B]) ReaderReaderIOResult[R, A] {
|
||||
return chain.MonadChainFirst(
|
||||
MonadChain[R, A, A],
|
||||
MonadMap[R, B, A],
|
||||
fa,
|
||||
f)
|
||||
}
|
||||
|
||||
// MonadTap is an alias for MonadChainFirst.
|
||||
// Executes a side effect while preserving the original value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadTap[R, A, B any](fa ReaderReaderIOResult[R, A], f Kleisli[R, A, B]) ReaderReaderIOResult[R, A] {
|
||||
return MonadChainFirst(fa, f)
|
||||
}
|
||||
|
||||
// MonadChainEitherK chains a computation that returns an Either.
|
||||
// The Either is automatically lifted into ReaderReaderIOResult.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainEitherK[R, A, B any](ma ReaderReaderIOResult[R, A], f either.Kleisli[error, A, B]) ReaderReaderIOResult[R, B] {
|
||||
return fromeither.MonadChainEitherK(
|
||||
MonadChain[R, A, B],
|
||||
FromEither[R, B],
|
||||
ma,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// ChainEitherK chains a computation that returns an Either.
|
||||
// The Either is automatically lifted into ReaderReaderIOResult.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainEitherK[R, A, B any](f either.Kleisli[error, A, B]) Operator[R, A, B] {
|
||||
return fromeither.ChainEitherK(
|
||||
Chain[R, A, B],
|
||||
FromEither[R, B],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadChainFirstEitherK chains a computation that returns an Either but preserves the original value.
|
||||
// Useful for validation or side effects that may fail.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstEitherK[R, A, B any](ma ReaderReaderIOResult[R, A], f either.Kleisli[error, A, B]) ReaderReaderIOResult[R, A] {
|
||||
return fromeither.MonadChainFirstEitherK(
|
||||
MonadChain[R, A, A],
|
||||
MonadMap[R, B, A],
|
||||
FromEither[R, B],
|
||||
ma,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadTapEitherK is an alias for MonadChainFirstEitherK.
|
||||
// Executes an Either-returning side effect while preserving the original value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadTapEitherK[R, A, B any](ma ReaderReaderIOResult[R, A], f either.Kleisli[error, A, B]) ReaderReaderIOResult[R, A] {
|
||||
return MonadChainFirstEitherK(ma, f)
|
||||
}
|
||||
|
||||
// ChainFirstEitherK chains a computation that returns an Either but preserves the original value.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstEitherK[R, A, B any](f either.Kleisli[error, A, B]) Operator[R, A, A] {
|
||||
return fromeither.ChainFirstEitherK(
|
||||
Chain[R, A, A],
|
||||
Map[R, B, A],
|
||||
FromEither[R, B],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// TapEitherK is an alias for ChainFirstEitherK.
|
||||
// Executes an Either-returning side effect while preserving the original value.
|
||||
//
|
||||
//go:inline
|
||||
func TapEitherK[R, A, B any](f either.Kleisli[error, A, B]) Operator[R, A, A] {
|
||||
return ChainFirstEitherK[R](f)
|
||||
}
|
||||
|
||||
// MonadChainReaderK chains a computation that returns a Reader.
|
||||
// The Reader is automatically lifted into ReaderReaderIOResult.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainReaderK[R, A, B any](ma ReaderReaderIOResult[R, A], f reader.Kleisli[R, A, B]) ReaderReaderIOResult[R, B] {
|
||||
return fromreader.MonadChainReaderK(
|
||||
MonadChain[R, A, B],
|
||||
FromReader[R, B],
|
||||
ma,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// ChainReaderK chains a computation that returns a Reader.
|
||||
// The Reader is automatically lifted into ReaderReaderIOResult.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, B] {
|
||||
return fromreader.ChainReaderK(
|
||||
Chain[R, A, B],
|
||||
FromReader[R, B],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadChainFirstReaderK chains a computation that returns a Reader but preserves the original value.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstReaderK[R, A, B any](ma ReaderReaderIOResult[R, A], f reader.Kleisli[R, A, B]) ReaderReaderIOResult[R, A] {
|
||||
return fromreader.MonadChainFirstReaderK(
|
||||
MonadChainFirst[R, A, B],
|
||||
FromReader[R, B],
|
||||
ma,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadTapReaderK is an alias for MonadChainFirstReaderK.
|
||||
// Executes a Reader-returning side effect while preserving the original value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadTapReaderK[R, A, B any](ma ReaderReaderIOResult[R, A], f reader.Kleisli[R, A, B]) ReaderReaderIOResult[R, A] {
|
||||
return MonadChainFirstReaderK(ma, f)
|
||||
}
|
||||
|
||||
// ChainFirstReaderK chains a computation that returns a Reader but preserves the original value.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, A] {
|
||||
return fromreader.ChainFirstReaderK(
|
||||
ChainFirst[R, A, B],
|
||||
FromReader[R, B],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// TapReaderK is an alias for ChainFirstReaderK.
|
||||
// Executes a Reader-returning side effect while preserving the original value.
|
||||
//
|
||||
//go:inline
|
||||
func TapReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, A] {
|
||||
return ChainFirstReaderK(f)
|
||||
}
|
||||
|
||||
// MonadChainReaderIOK chains a computation that returns a ReaderIO.
|
||||
// The ReaderIO is automatically lifted into ReaderReaderIOResult.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainReaderIOK[R, A, B any](ma ReaderReaderIOResult[R, A], f readerio.Kleisli[R, A, B]) ReaderReaderIOResult[R, B] {
|
||||
return fromreader.MonadChainReaderK(
|
||||
MonadChain[R, A, B],
|
||||
FromReaderIO[R, B],
|
||||
ma,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// ChainReaderIOK chains a computation that returns a ReaderIO.
|
||||
// The ReaderIO is automatically lifted into ReaderReaderIOResult.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainReaderIOK[R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, A, B] {
|
||||
return fromreader.ChainReaderK(
|
||||
Chain[R, A, B],
|
||||
FromReaderIO[R, B],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadChainFirstReaderIOK chains a computation that returns a ReaderIO but preserves the original value.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstReaderIOK[R, A, B any](ma ReaderReaderIOResult[R, A], f readerio.Kleisli[R, A, B]) ReaderReaderIOResult[R, A] {
|
||||
return fromreader.MonadChainFirstReaderK(
|
||||
MonadChainFirst[R, A, B],
|
||||
FromReaderIO[R, B],
|
||||
ma,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadTapReaderIOK is an alias for MonadChainFirstReaderIOK.
|
||||
// Executes a ReaderIO-returning side effect while preserving the original value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadTapReaderIOK[R, A, B any](ma ReaderReaderIOResult[R, A], f readerio.Kleisli[R, A, B]) ReaderReaderIOResult[R, A] {
|
||||
return MonadChainFirstReaderIOK(ma, f)
|
||||
}
|
||||
|
||||
// ChainFirstReaderIOK chains a computation that returns a ReaderIO but preserves the original value.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstReaderIOK[R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, A, A] {
|
||||
return fromreader.ChainFirstReaderK(
|
||||
ChainFirst[R, A, B],
|
||||
FromReaderIO[R, B],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// TapReaderIOK is an alias for ChainFirstReaderIOK.
|
||||
// Executes a ReaderIO-returning side effect while preserving the original value.
|
||||
//
|
||||
//go:inline
|
||||
func TapReaderIOK[R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, A, A] {
|
||||
return ChainFirstReaderIOK(f)
|
||||
}
|
||||
|
||||
// MonadChainReaderEitherK chains a computation that returns a ReaderEither.
|
||||
// The ReaderEither is automatically lifted into ReaderReaderIOResult.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainReaderEitherK[R, A, B any](ma ReaderReaderIOResult[R, A], f RE.Kleisli[R, error, A, B]) ReaderReaderIOResult[R, B] {
|
||||
return fromreader.MonadChainReaderK(
|
||||
MonadChain[R, A, B],
|
||||
FromReaderEither[R, B],
|
||||
ma,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// ChainReaderEitherK chains a computation that returns a ReaderEither.
|
||||
// The ReaderEither is automatically lifted into ReaderReaderIOResult.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainReaderEitherK[R, A, B any](f RE.Kleisli[R, error, A, B]) Operator[R, A, B] {
|
||||
return fromreader.ChainReaderK(
|
||||
Chain[R, A, B],
|
||||
FromReaderEither[R, B],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadChainFirstReaderEitherK chains a computation that returns a ReaderEither but preserves the original value.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstReaderEitherK[R, A, B any](ma ReaderReaderIOResult[R, A], f RE.Kleisli[R, error, A, B]) ReaderReaderIOResult[R, A] {
|
||||
return fromreader.MonadChainFirstReaderK(
|
||||
MonadChainFirst[R, A, B],
|
||||
FromReaderEither[R, B],
|
||||
ma,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadTapReaderEitherK is an alias for MonadChainFirstReaderEitherK.
|
||||
// Executes a ReaderEither-returning side effect while preserving the original value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadTapReaderEitherK[R, A, B any](ma ReaderReaderIOResult[R, A], f RE.Kleisli[R, error, A, B]) ReaderReaderIOResult[R, A] {
|
||||
return MonadChainFirstReaderEitherK(ma, f)
|
||||
}
|
||||
|
||||
// ChainFirstReaderEitherK chains a computation that returns a ReaderEither but preserves the original value.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstReaderEitherK[R, A, B any](f RE.Kleisli[R, error, A, B]) Operator[R, A, A] {
|
||||
return fromreader.ChainFirstReaderK(
|
||||
ChainFirst[R, A, B],
|
||||
FromReaderEither[R, B],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// TapReaderEitherK is an alias for ChainFirstReaderEitherK.
|
||||
// Executes a ReaderEither-returning side effect while preserving the original value.
|
||||
//
|
||||
//go:inline
|
||||
func TapReaderEitherK[R, A, B any](f RE.Kleisli[R, error, A, B]) Operator[R, A, A] {
|
||||
return ChainFirstReaderEitherK(f)
|
||||
}
|
||||
|
||||
// ChainReaderOptionK chains a computation that returns a ReaderOption.
|
||||
// If the option is None, it uses the provided onNone function to generate an error.
|
||||
// Returns a function that takes a ReaderOption Kleisli and returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainReaderOptionK[R, A, B any](onNone Lazy[error]) func(readeroption.Kleisli[R, A, B]) Operator[R, A, B] {
|
||||
return RRIOE.ChainReaderOptionK[R, context.Context, A, B](onNone)
|
||||
}
|
||||
|
||||
// ChainFirstReaderOptionK chains a computation that returns a ReaderOption but preserves the original value.
|
||||
// If the option is None, it uses the provided onNone function to generate an error.
|
||||
// Returns a function that takes a ReaderOption Kleisli and returns an operator.
|
||||
func ChainFirstReaderOptionK[R, A, B any](onNone Lazy[error]) func(readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
|
||||
return RRIOE.ChainFirstReaderOptionK[R, context.Context, A, B](onNone)
|
||||
}
|
||||
|
||||
// TapReaderOptionK is an alias for ChainFirstReaderOptionK.
|
||||
// Executes a ReaderOption-returning side effect while preserving the original value.
|
||||
//
|
||||
//go:inline
|
||||
func TapReaderOptionK[R, A, B any](onNone Lazy[error]) func(readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
|
||||
return ChainFirstReaderOptionK[R, A, B](onNone)
|
||||
}
|
||||
|
||||
// MonadChainIOEitherK chains a computation that returns an IOEither.
|
||||
// The IOEither is automatically lifted into ReaderReaderIOResult.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainIOEitherK[R, A, B any](ma ReaderReaderIOResult[R, A], f IOE.Kleisli[error, A, B]) ReaderReaderIOResult[R, B] {
|
||||
return fromioeither.MonadChainIOEitherK(
|
||||
MonadChain[R, A, B],
|
||||
FromIOEither[R, B],
|
||||
ma,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// ChainIOEitherK chains a computation that returns an IOEither.
|
||||
// The IOEither is automatically lifted into ReaderReaderIOResult.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainIOEitherK[R, A, B any](f IOE.Kleisli[error, A, B]) Operator[R, A, B] {
|
||||
return fromioeither.ChainIOEitherK(
|
||||
Chain[R, A, B],
|
||||
FromIOEither[R, B],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadChainIOK chains a computation that returns an IO.
|
||||
// The IO is automatically lifted into ReaderReaderIOResult.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainIOK[R, A, B any](ma ReaderReaderIOResult[R, A], f io.Kleisli[A, B]) ReaderReaderIOResult[R, B] {
|
||||
return fromio.MonadChainIOK(
|
||||
MonadChain[R, A, B],
|
||||
FromIO[R, B],
|
||||
ma,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// ChainIOK chains a computation that returns an IO.
|
||||
// The IO is automatically lifted into ReaderReaderIOResult.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainIOK[R, A, B any](f io.Kleisli[A, B]) Operator[R, A, B] {
|
||||
return fromio.ChainIOK(
|
||||
Chain[R, A, B],
|
||||
FromIO[R, B],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadChainFirstIOK chains a computation that returns an IO but preserves the original value.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstIOK[R, A, B any](ma ReaderReaderIOResult[R, A], f io.Kleisli[A, B]) ReaderReaderIOResult[R, A] {
|
||||
return fromio.MonadChainFirstIOK(
|
||||
MonadChain[R, A, A],
|
||||
MonadMap[R, B, A],
|
||||
FromIO[R, B],
|
||||
ma,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadTapIOK is an alias for MonadChainFirstIOK.
|
||||
// Executes an IO-returning side effect while preserving the original value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadTapIOK[R, A, B any](ma ReaderReaderIOResult[R, A], f io.Kleisli[A, B]) ReaderReaderIOResult[R, A] {
|
||||
return MonadChainFirstIOK(ma, f)
|
||||
}
|
||||
|
||||
// ChainFirstIOK chains a computation that returns an IO but preserves the original value.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstIOK[R, A, B any](f io.Kleisli[A, B]) Operator[R, A, A] {
|
||||
return fromio.ChainFirstIOK(
|
||||
Chain[R, A, A],
|
||||
Map[R, B, A],
|
||||
FromIO[R, B],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// TapIOK is an alias for ChainFirstIOK.
|
||||
// Executes an IO-returning side effect while preserving the original value.
|
||||
//
|
||||
//go:inline
|
||||
func TapIOK[R, A, B any](f io.Kleisli[A, B]) Operator[R, A, A] {
|
||||
return ChainFirstIOK[R](f)
|
||||
}
|
||||
|
||||
// ChainOptionK chains a computation that returns an Option.
|
||||
// If the option is None, it uses the provided onNone function to generate an error.
|
||||
// Returns a function that takes an Option Kleisli and returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainOptionK[R, A, B any](onNone Lazy[error]) func(option.Kleisli[A, B]) Operator[R, A, B] {
|
||||
return fromeither.ChainOptionK(
|
||||
MonadChain[R, A, B],
|
||||
FromEither[R, B],
|
||||
onNone,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadAp applies a function wrapped in a ReaderReaderIOResult to a value wrapped in a ReaderReaderIOResult (Applicative operation).
|
||||
// This is the monadic version that takes both computations as parameters.
|
||||
//
|
||||
//go:inline
|
||||
func MonadAp[R, A, B any](fab ReaderReaderIOResult[R, func(A) B], fa ReaderReaderIOResult[R, A]) ReaderReaderIOResult[R, B] {
|
||||
return readert.MonadAp[
|
||||
ReaderReaderIOResult[R, A],
|
||||
ReaderReaderIOResult[R, B],
|
||||
ReaderReaderIOResult[R, func(A) B], R, A](
|
||||
RIOE.MonadAp[B, A],
|
||||
fab,
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadApSeq is like MonadAp but evaluates effects sequentially.
|
||||
//
|
||||
//go:inline
|
||||
func MonadApSeq[R, A, B any](fab ReaderReaderIOResult[R, func(A) B], fa ReaderReaderIOResult[R, A]) ReaderReaderIOResult[R, B] {
|
||||
return readert.MonadAp[
|
||||
ReaderReaderIOResult[R, A],
|
||||
ReaderReaderIOResult[R, B],
|
||||
ReaderReaderIOResult[R, func(A) B], R, A](
|
||||
RIOE.MonadApSeq[B, A],
|
||||
fab,
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadApPar is like MonadAp but evaluates effects in parallel.
|
||||
//
|
||||
//go:inline
|
||||
func MonadApPar[R, A, B any](fab ReaderReaderIOResult[R, func(A) B], fa ReaderReaderIOResult[R, A]) ReaderReaderIOResult[R, B] {
|
||||
return readert.MonadAp[
|
||||
ReaderReaderIOResult[R, A],
|
||||
ReaderReaderIOResult[R, B],
|
||||
ReaderReaderIOResult[R, func(A) B], R, A](
|
||||
RIOE.MonadApPar[B, A],
|
||||
fab,
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// Ap applies a function wrapped in a ReaderReaderIOResult to a value wrapped in a ReaderReaderIOResult.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func Ap[B, R, A any](fa ReaderReaderIOResult[R, A]) Operator[R, func(A) B, B] {
|
||||
return readert.Ap[
|
||||
ReaderReaderIOResult[R, A],
|
||||
ReaderReaderIOResult[R, B],
|
||||
ReaderReaderIOResult[R, func(A) B], R, A](
|
||||
RIOE.Ap[B, A],
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// Chain sequences two computations, where the second depends on the result of the first (Monad operation).
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func Chain[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, B] {
|
||||
return readert.Chain[ReaderReaderIOResult[R, A]](
|
||||
RIOE.Chain[A, B],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// ChainFirst sequences two computations but returns the result of the first.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirst[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, A] {
|
||||
return chain.ChainFirst(
|
||||
Chain[R, A, A],
|
||||
Map[R, B, A],
|
||||
f)
|
||||
}
|
||||
|
||||
// Tap is an alias for ChainFirst.
|
||||
// Executes a side effect while preserving the original value.
|
||||
//
|
||||
//go:inline
|
||||
func Tap[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, A] {
|
||||
return ChainFirst(f)
|
||||
}
|
||||
|
||||
// Right creates a ReaderReaderIOResult that succeeds with the given value.
|
||||
// This is the success constructor for the Result type.
|
||||
//
|
||||
//go:inline
|
||||
func Right[R, A any](a A) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.Right[R, context.Context, error](a)
|
||||
}
|
||||
|
||||
// Left creates a ReaderReaderIOResult that fails with the given error.
|
||||
// This is the failure constructor for the Result type.
|
||||
//
|
||||
//go:inline
|
||||
func Left[R, A any](e error) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.Left[R, context.Context, A](e)
|
||||
}
|
||||
|
||||
// Of creates a ReaderReaderIOResult that succeeds with the given value (Pointed operation).
|
||||
// Alias for Right.
|
||||
//
|
||||
//go:inline
|
||||
func Of[R, A any](a A) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.Of[R, context.Context, error](a)
|
||||
}
|
||||
|
||||
// Flatten removes one level of nesting from a nested ReaderReaderIOResult.
|
||||
// Converts ReaderReaderIOResult[R, ReaderReaderIOResult[R, A]] to ReaderReaderIOResult[R, A].
|
||||
//
|
||||
//go:inline
|
||||
func Flatten[R, A any](mma ReaderReaderIOResult[R, ReaderReaderIOResult[R, A]]) ReaderReaderIOResult[R, A] {
|
||||
return MonadChain(mma, function.Identity[ReaderReaderIOResult[R, A]])
|
||||
}
|
||||
|
||||
// FromEither lifts an Either into a ReaderReaderIOResult.
|
||||
//
|
||||
//go:inline
|
||||
func FromEither[R, A any](t Either[error, A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.FromEither[R, context.Context](t)
|
||||
}
|
||||
|
||||
// FromResult lifts a Result into a ReaderReaderIOResult.
|
||||
// Alias for FromEither since Result is Either[error, A].
|
||||
//
|
||||
//go:inline
|
||||
func FromResult[R, A any](t Result[A]) ReaderReaderIOResult[R, A] {
|
||||
return FromEither[R](t)
|
||||
}
|
||||
|
||||
// RightReader lifts a Reader into a ReaderReaderIOResult as a Right (success) value.
|
||||
//
|
||||
//go:inline
|
||||
func RightReader[R, A any](ma Reader[R, A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.RightReader[context.Context, error](ma)
|
||||
}
|
||||
|
||||
// LeftReader lifts a Reader that produces an error into a ReaderReaderIOResult as a Left (failure) value.
|
||||
//
|
||||
//go:inline
|
||||
func LeftReader[A, R any](ma Reader[R, error]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.LeftReader[context.Context, A](ma)
|
||||
}
|
||||
|
||||
// FromReader lifts a Reader into a ReaderReaderIOResult.
|
||||
// The Reader's result is wrapped in a Right (success) value.
|
||||
//
|
||||
//go:inline
|
||||
func FromReader[R, A any](ma Reader[R, A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.FromReader[context.Context, error](ma)
|
||||
}
|
||||
|
||||
// RightIO lifts an IO into a ReaderReaderIOResult as a Right (success) value.
|
||||
//
|
||||
//go:inline
|
||||
func RightIO[R, A any](ma IO[A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.RightIO[R, context.Context, error](ma)
|
||||
}
|
||||
|
||||
// LeftIO lifts an IO that produces an error into a ReaderReaderIOResult as a Left (failure) value.
|
||||
//
|
||||
//go:inline
|
||||
func LeftIO[R, A any](ma IO[error]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.LeftIO[R, context.Context, A](ma)
|
||||
}
|
||||
|
||||
// FromIO lifts an IO into a ReaderReaderIOResult.
|
||||
// The IO's result is wrapped in a Right (success) value.
|
||||
//
|
||||
//go:inline
|
||||
func FromIO[R, A any](ma IO[A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.FromIO[R, context.Context, error](ma)
|
||||
}
|
||||
|
||||
// FromIOEither lifts an IOEither into a ReaderReaderIOResult.
|
||||
//
|
||||
//go:inline
|
||||
func FromIOEither[R, A any](ma IOEither[error, A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.FromIOEither[R, context.Context](ma)
|
||||
}
|
||||
|
||||
// FromIOResult lifts an IOResult into a ReaderReaderIOResult.
|
||||
// Alias for FromIOEither since IOResult is IOEither[error, A].
|
||||
//
|
||||
//go:inline
|
||||
func FromIOResult[R, A any](ma IOResult[A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.FromIOEither[R, context.Context](ma)
|
||||
}
|
||||
|
||||
// FromReaderEither lifts a ReaderEither into a ReaderReaderIOResult.
|
||||
//
|
||||
//go:inline
|
||||
func FromReaderEither[R, A any](ma RE.ReaderEither[R, error, A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.FromReaderEither[R, context.Context](ma)
|
||||
}
|
||||
|
||||
// Ask retrieves the outer environment R.
|
||||
// Returns a ReaderReaderIOResult that succeeds with the environment value.
|
||||
//
|
||||
//go:inline
|
||||
func Ask[R any]() ReaderReaderIOResult[R, R] {
|
||||
return RRIOE.Ask[R, context.Context, error]()
|
||||
}
|
||||
|
||||
// Asks retrieves a value derived from the outer environment R using the provided function.
|
||||
//
|
||||
//go:inline
|
||||
func Asks[R, A any](r Reader[R, A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.Asks[context.Context, error](r)
|
||||
}
|
||||
|
||||
// FromOption converts an Option to a ReaderReaderIOResult.
|
||||
// If the option is None, it uses the provided onNone function to generate an error.
|
||||
// Returns a function that takes an Option and returns a ReaderReaderIOResult.
|
||||
//
|
||||
//go:inline
|
||||
func FromOption[R, A any](onNone Lazy[error]) func(Option[A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.FromOption[R, context.Context, A](onNone)
|
||||
}
|
||||
|
||||
// FromPredicate creates a ReaderReaderIOResult from a predicate.
|
||||
// If the predicate returns true, the value is wrapped in Right.
|
||||
// If false, onFalse is called to generate an error wrapped in Left.
|
||||
//
|
||||
//go:inline
|
||||
func FromPredicate[R, A any](pred func(A) bool, onFalse func(A) error) Kleisli[R, A, A] {
|
||||
return RRIOE.FromPredicate[R, context.Context](pred, onFalse)
|
||||
}
|
||||
|
||||
// MonadAlt provides alternative/fallback behavior.
|
||||
// If the first computation fails, it tries the second (lazy-evaluated).
|
||||
// This is the monadic version that takes both computations as parameters.
|
||||
//
|
||||
//go:inline
|
||||
func MonadAlt[R, A any](first ReaderReaderIOResult[R, A], second Lazy[ReaderReaderIOResult[R, A]]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.MonadAlt(first, second)
|
||||
}
|
||||
|
||||
// Alt provides alternative/fallback behavior.
|
||||
// If the first computation fails, it tries the second (lazy-evaluated).
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func Alt[R, A any](second Lazy[ReaderReaderIOResult[R, A]]) Operator[R, A, A] {
|
||||
return RRIOE.Alt(second)
|
||||
}
|
||||
|
||||
// MonadFlap applies a value to a function wrapped in a ReaderReaderIOResult.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadFlap[R, B, A any](fab ReaderReaderIOResult[R, func(A) B], a A) ReaderReaderIOResult[R, B] {
|
||||
return functor.MonadFlap(MonadMap[R, func(A) B, B], fab, a)
|
||||
}
|
||||
|
||||
// Flap applies a value to a function wrapped in a ReaderReaderIOResult.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func Flap[R, B, A any](a A) Operator[R, func(A) B, B] {
|
||||
return functor.Flap(Map[R, func(A) B, B], a)
|
||||
}
|
||||
|
||||
// MonadMapLeft transforms the error value if the computation fails.
|
||||
// Has no effect if the computation succeeds.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadMapLeft[R, A any](fa ReaderReaderIOResult[R, A], f Endmorphism[error]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.MonadMapLeft(fa, f)
|
||||
}
|
||||
|
||||
// MapLeft transforms the error value if the computation fails.
|
||||
// Has no effect if the computation succeeds.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func MapLeft[R, A any](f Endmorphism[error]) Operator[R, A, A] {
|
||||
return RRIOE.MapLeft[R, context.Context, A](f)
|
||||
}
|
||||
|
||||
// Local modifies the outer environment before passing it to a computation.
|
||||
// Useful for providing different configurations to sub-computations.
|
||||
//
|
||||
//go:inline
|
||||
func Local[A, R1, R2 any](f func(R2) R1) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
|
||||
return RRIOE.Local[context.Context, error, A](f)
|
||||
}
|
||||
|
||||
// Read provides a specific outer environment value to a computation.
|
||||
// Converts ReaderReaderIOResult[R, A] to ReaderIOResult[context.Context, A].
|
||||
//
|
||||
//go:inline
|
||||
func Read[A, R any](r R) func(ReaderReaderIOResult[R, A]) ReaderIOResult[context.Context, A] {
|
||||
return RRIOE.Read[context.Context, error, A](r)
|
||||
}
|
||||
|
||||
// ReadIOEither provides an outer environment value from an IOEither to a computation.
|
||||
//
|
||||
//go:inline
|
||||
func ReadIOEither[A, R any](rio IOEither[error, R]) func(ReaderReaderIOResult[R, A]) ReaderIOResult[context.Context, A] {
|
||||
return RRIOE.ReadIOEither[A, R, context.Context](rio)
|
||||
}
|
||||
|
||||
// ReadIO provides an outer environment value from an IO to a computation.
|
||||
//
|
||||
//go:inline
|
||||
func ReadIO[A, R any](rio IO[R]) func(ReaderReaderIOResult[R, A]) ReaderIOResult[context.Context, A] {
|
||||
return RRIOE.ReadIO[context.Context, error, A](rio)
|
||||
}
|
||||
|
||||
// MonadChainLeft handles errors by chaining a recovery computation.
|
||||
// If the computation fails, the error is passed to f for recovery.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainLeft[R, A any](fa ReaderReaderIOResult[R, A], f Kleisli[R, error, A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.MonadChainLeft(fa, f)
|
||||
}
|
||||
|
||||
// ChainLeft handles errors by chaining a recovery computation.
|
||||
// If the computation fails, the error is passed to f for recovery.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainLeft[R, A any](f Kleisli[R, error, A]) func(ReaderReaderIOResult[R, A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.ChainLeft(f)
|
||||
}
|
||||
|
||||
// Delay adds a time delay before executing the computation.
|
||||
// Useful for rate limiting, retry backoff, or scheduled execution.
|
||||
//
|
||||
//go:inline
|
||||
func Delay[R, A any](delay time.Duration) Operator[R, A, A] {
|
||||
return reader.Map[R](RIOE.Delay[A](delay))
|
||||
}
|
||||
718
v2/context/readerreaderioresult/reader_test.go
Normal file
718
v2/context/readerreaderioresult/reader_test.go
Normal file
@@ -0,0 +1,718 @@
|
||||
// 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 readerreaderioresult
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "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"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
RE "github.com/IBM/fp-go/v2/readereither"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
"github.com/IBM/fp-go/v2/readeroption"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOf(t *testing.T) {
|
||||
computation := Of[AppConfig](42)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
}
|
||||
|
||||
func TestRight(t *testing.T) {
|
||||
computation := Right[AppConfig](42)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
}
|
||||
|
||||
func TestLeft(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
computation := Left[AppConfig, int](err)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Left[int](err), outcome)
|
||||
}
|
||||
|
||||
func TestMonadMap(t *testing.T) {
|
||||
computation := MonadMap(
|
||||
Of[AppConfig](21),
|
||||
N.Mul(2),
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
}
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](21),
|
||||
Map[AppConfig](N.Mul(2)),
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
}
|
||||
|
||||
func TestMonadMapTo(t *testing.T) {
|
||||
computation := MonadMapTo(Of[AppConfig](21), 99)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(99), outcome)
|
||||
}
|
||||
|
||||
func TestMapTo(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](21),
|
||||
MapTo[AppConfig, int](99),
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(99), outcome)
|
||||
}
|
||||
|
||||
func TestMonadChain(t *testing.T) {
|
||||
computation := MonadChain(
|
||||
Of[AppConfig](21),
|
||||
func(n int) ReaderReaderIOResult[AppConfig, int] {
|
||||
return Of[AppConfig](n * 2)
|
||||
},
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
}
|
||||
|
||||
func TestChain(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](21),
|
||||
Chain(func(n int) ReaderReaderIOResult[AppConfig, int] {
|
||||
return Of[AppConfig](n * 2)
|
||||
}),
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
}
|
||||
|
||||
func TestMonadChainFirst(t *testing.T) {
|
||||
sideEffect := 0
|
||||
computation := MonadChainFirst(
|
||||
Of[AppConfig](42),
|
||||
func(n int) ReaderReaderIOResult[AppConfig, string] {
|
||||
sideEffect = n
|
||||
return Of[AppConfig]("ignored")
|
||||
},
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestChainFirst(t *testing.T) {
|
||||
sideEffect := 0
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](42),
|
||||
ChainFirst(func(n int) ReaderReaderIOResult[AppConfig, string] {
|
||||
sideEffect = n
|
||||
return Of[AppConfig]("ignored")
|
||||
}),
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestTap(t *testing.T) {
|
||||
sideEffect := 0
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](42),
|
||||
Tap(func(n int) ReaderReaderIOResult[AppConfig, string] {
|
||||
sideEffect = n
|
||||
return Of[AppConfig]("ignored")
|
||||
}),
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestFlatten(t *testing.T) {
|
||||
nested := Of[AppConfig](Of[AppConfig](42))
|
||||
computation := Flatten(nested)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
}
|
||||
|
||||
func TestFromEither(t *testing.T) {
|
||||
t.Run("right", func(t *testing.T) {
|
||||
computation := FromEither[AppConfig](either.Right[error](42))
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
})
|
||||
|
||||
t.Run("left", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
computation := FromEither[AppConfig](either.Left[int](err))
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromResult(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
computation := FromResult[AppConfig](result.Of(42))
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
})
|
||||
|
||||
t.Run("error", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
computation := FromResult[AppConfig](result.Left[int](err))
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromReader(t *testing.T) {
|
||||
computation := FromReader(func(cfg AppConfig) int {
|
||||
return len(cfg.DatabaseURL)
|
||||
})
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(20), outcome) // len("postgres://localhost")
|
||||
}
|
||||
|
||||
func TestRightReader(t *testing.T) {
|
||||
computation := RightReader(func(cfg AppConfig) int {
|
||||
return len(cfg.LogLevel)
|
||||
})
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(4), outcome) // len("info")
|
||||
}
|
||||
|
||||
func TestLeftReader(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
computation := LeftReader[int](func(cfg AppConfig) error {
|
||||
return err
|
||||
})
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
}
|
||||
|
||||
func TestFromIO(t *testing.T) {
|
||||
computation := FromIO[AppConfig](func() int { return 42 })
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
}
|
||||
|
||||
func TestRightIO(t *testing.T) {
|
||||
computation := RightIO[AppConfig](func() int { return 42 })
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
}
|
||||
|
||||
func TestLeftIO(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
computation := LeftIO[AppConfig, int](func() error { return err })
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
}
|
||||
|
||||
func TestFromIOEither(t *testing.T) {
|
||||
t.Run("right", func(t *testing.T) {
|
||||
computation := FromIOEither[AppConfig](ioeither.Of[error](42))
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
})
|
||||
|
||||
t.Run("left", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
computation := FromIOEither[AppConfig](ioeither.Left[int](err))
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIOResult(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
computation := FromIOResult[AppConfig](func() result.Result[int] {
|
||||
return result.Of(42)
|
||||
})
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
})
|
||||
|
||||
t.Run("error", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
computation := FromIOResult[AppConfig](func() result.Result[int] {
|
||||
return result.Left[int](err)
|
||||
})
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromReaderIO(t *testing.T) {
|
||||
computation := FromReaderIO(func(cfg AppConfig) io.IO[int] {
|
||||
return func() int { return len(cfg.DatabaseURL) }
|
||||
})
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(20), outcome)
|
||||
}
|
||||
|
||||
func TestRightReaderIO(t *testing.T) {
|
||||
computation := RightReaderIO(func(cfg AppConfig) io.IO[int] {
|
||||
return func() int { return len(cfg.LogLevel) }
|
||||
})
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(4), outcome)
|
||||
}
|
||||
|
||||
func TestLeftReaderIO(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
computation := LeftReaderIO[int](func(cfg AppConfig) io.IO[error] {
|
||||
return func() error { return err }
|
||||
})
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
}
|
||||
|
||||
func TestFromReaderEither(t *testing.T) {
|
||||
t.Run("right", func(t *testing.T) {
|
||||
computation := FromReaderEither(func(cfg AppConfig) either.Either[error, int] {
|
||||
return either.Right[error](len(cfg.DatabaseURL))
|
||||
})
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(20), outcome)
|
||||
})
|
||||
|
||||
t.Run("left", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
computation := FromReaderEither(func(cfg AppConfig) either.Either[error, int] {
|
||||
return either.Left[int](err)
|
||||
})
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
})
|
||||
}
|
||||
|
||||
func TestAsk(t *testing.T) {
|
||||
computation := Ask[AppConfig]()
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(defaultConfig), outcome)
|
||||
}
|
||||
|
||||
func TestAsks(t *testing.T) {
|
||||
computation := Asks(func(cfg AppConfig) string {
|
||||
return cfg.DatabaseURL
|
||||
})
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of("postgres://localhost"), outcome)
|
||||
}
|
||||
|
||||
func TestFromOption(t *testing.T) {
|
||||
err := errors.New("none error")
|
||||
|
||||
t.Run("some", func(t *testing.T) {
|
||||
computation := FromOption[AppConfig, int](func() error { return err })(option.Some(42))
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
})
|
||||
|
||||
t.Run("none", func(t *testing.T) {
|
||||
computation := FromOption[AppConfig, int](func() error { return err })(option.None[int]())
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromPredicate(t *testing.T) {
|
||||
isPositive := func(n int) bool { return n > 0 }
|
||||
onFalse := func(n int) error { return errors.New("not positive") }
|
||||
|
||||
t.Run("predicate true", func(t *testing.T) {
|
||||
computation := FromPredicate[AppConfig](isPositive, onFalse)(42)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
})
|
||||
|
||||
t.Run("predicate false", func(t *testing.T) {
|
||||
computation := FromPredicate[AppConfig](isPositive, onFalse)(-5)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadAlt(t *testing.T) {
|
||||
err := errors.New("first error")
|
||||
|
||||
t.Run("first succeeds", func(t *testing.T) {
|
||||
first := Of[AppConfig](42)
|
||||
second := func() ReaderReaderIOResult[AppConfig, int] {
|
||||
return Of[AppConfig](99)
|
||||
}
|
||||
computation := MonadAlt(first, second)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
})
|
||||
|
||||
t.Run("first fails, second succeeds", func(t *testing.T) {
|
||||
first := Left[AppConfig, int](err)
|
||||
second := func() ReaderReaderIOResult[AppConfig, int] {
|
||||
return Of[AppConfig](99)
|
||||
}
|
||||
computation := MonadAlt(first, second)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(99), outcome)
|
||||
})
|
||||
|
||||
t.Run("both fail", func(t *testing.T) {
|
||||
first := Left[AppConfig, int](err)
|
||||
second := func() ReaderReaderIOResult[AppConfig, int] {
|
||||
return Left[AppConfig, int](errors.New("second error"))
|
||||
}
|
||||
computation := MonadAlt(first, second)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
})
|
||||
}
|
||||
|
||||
func TestAlt(t *testing.T) {
|
||||
err := errors.New("first error")
|
||||
|
||||
computation := F.Pipe1(
|
||||
Left[AppConfig, int](err),
|
||||
Alt(func() ReaderReaderIOResult[AppConfig, int] {
|
||||
return Of[AppConfig](99)
|
||||
}),
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(99), outcome)
|
||||
}
|
||||
|
||||
func TestMonadFlap(t *testing.T) {
|
||||
fab := Of[AppConfig](N.Mul(2))
|
||||
computation := MonadFlap(fab, 21)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
}
|
||||
|
||||
func TestFlap(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](N.Mul(2)),
|
||||
Flap[AppConfig, int](21),
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
}
|
||||
|
||||
func TestMonadMapLeft(t *testing.T) {
|
||||
err := errors.New("original error")
|
||||
computation := MonadMapLeft(
|
||||
Left[AppConfig, int](err),
|
||||
func(e error) error { return errors.New("mapped: " + e.Error()) },
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
result.Fold(
|
||||
func(e error) any {
|
||||
assert.Contains(t, e.Error(), "mapped:")
|
||||
return nil
|
||||
},
|
||||
func(v int) any {
|
||||
t.Fatal("should be left")
|
||||
return nil
|
||||
},
|
||||
)(outcome)
|
||||
}
|
||||
|
||||
func TestMapLeft(t *testing.T) {
|
||||
err := errors.New("original error")
|
||||
computation := F.Pipe1(
|
||||
Left[AppConfig, int](err),
|
||||
MapLeft[AppConfig, int](func(e error) error {
|
||||
return errors.New("mapped: " + e.Error())
|
||||
}),
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
}
|
||||
|
||||
func TestLocal(t *testing.T) {
|
||||
type OtherConfig struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Asks(func(cfg AppConfig) string {
|
||||
return cfg.DatabaseURL
|
||||
}),
|
||||
Local[string](func(other OtherConfig) AppConfig {
|
||||
return AppConfig{DatabaseURL: other.URL, LogLevel: "debug"}
|
||||
}),
|
||||
)
|
||||
|
||||
outcome := computation(OtherConfig{URL: "test-url"})(t.Context())()
|
||||
assert.Equal(t, result.Of("test-url"), outcome)
|
||||
}
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
computation := Asks(func(cfg AppConfig) string {
|
||||
return cfg.DatabaseURL
|
||||
})
|
||||
|
||||
reader := Read[string](defaultConfig)
|
||||
outcome := reader(computation)(t.Context())()
|
||||
assert.Equal(t, result.Of("postgres://localhost"), outcome)
|
||||
}
|
||||
|
||||
func TestReadIOEither(t *testing.T) {
|
||||
computation := Asks(func(cfg AppConfig) string {
|
||||
return cfg.DatabaseURL
|
||||
})
|
||||
|
||||
rio := ioeither.Of[error](defaultConfig)
|
||||
reader := ReadIOEither[string](rio)
|
||||
outcome := reader(computation)(t.Context())()
|
||||
assert.Equal(t, result.Of("postgres://localhost"), outcome)
|
||||
}
|
||||
|
||||
func TestReadIO(t *testing.T) {
|
||||
computation := Asks(func(cfg AppConfig) string {
|
||||
return cfg.DatabaseURL
|
||||
})
|
||||
|
||||
rio := func() AppConfig { return defaultConfig }
|
||||
reader := ReadIO[string](rio)
|
||||
outcome := reader(computation)(t.Context())()
|
||||
assert.Equal(t, result.Of("postgres://localhost"), outcome)
|
||||
}
|
||||
|
||||
func TestMonadChainLeft(t *testing.T) {
|
||||
err := errors.New("original error")
|
||||
computation := MonadChainLeft(
|
||||
Left[AppConfig, int](err),
|
||||
func(e error) ReaderReaderIOResult[AppConfig, int] {
|
||||
return Of[AppConfig](99)
|
||||
},
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(99), outcome)
|
||||
}
|
||||
|
||||
func TestChainLeft(t *testing.T) {
|
||||
err := errors.New("original error")
|
||||
computation := F.Pipe1(
|
||||
Left[AppConfig, int](err),
|
||||
ChainLeft(func(e error) ReaderReaderIOResult[AppConfig, int] {
|
||||
return Of[AppConfig](99)
|
||||
}),
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(99), outcome)
|
||||
}
|
||||
|
||||
func TestDelay(t *testing.T) {
|
||||
start := time.Now()
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](42),
|
||||
Delay[AppConfig, int](50*time.Millisecond),
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
elapsed := time.Since(start)
|
||||
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
assert.GreaterOrEqual(t, elapsed, 50*time.Millisecond)
|
||||
}
|
||||
|
||||
func TestChainEitherK(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](21),
|
||||
ChainEitherK[AppConfig](func(n int) either.Either[error, int] {
|
||||
return either.Right[error](n * 2)
|
||||
}),
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
}
|
||||
|
||||
func TestChainReaderK(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](10),
|
||||
ChainReaderK(func(n int) reader.Reader[AppConfig, int] {
|
||||
return func(cfg AppConfig) int {
|
||||
return n + len(cfg.LogLevel)
|
||||
}
|
||||
}),
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(14), outcome) // 10 + len("info")
|
||||
}
|
||||
|
||||
func TestChainReaderIOK(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](10),
|
||||
ChainReaderIOK(func(n int) readerio.ReaderIO[AppConfig, int] {
|
||||
return func(cfg AppConfig) io.IO[int] {
|
||||
return func() int {
|
||||
return n + len(cfg.DatabaseURL)
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(30), outcome) // 10 + 20
|
||||
}
|
||||
|
||||
func TestChainReaderEitherK(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](10),
|
||||
ChainReaderEitherK(func(n int) RE.ReaderEither[AppConfig, error, int] {
|
||||
return func(cfg AppConfig) either.Either[error, int] {
|
||||
return either.Right[error](n + len(cfg.LogLevel))
|
||||
}
|
||||
}),
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(14), outcome)
|
||||
}
|
||||
|
||||
func TestChainReaderOptionK(t *testing.T) {
|
||||
onNone := func() error { return errors.New("none") }
|
||||
|
||||
t.Run("some", func(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](10),
|
||||
ChainReaderOptionK[AppConfig, int, int](onNone)(func(n int) readeroption.ReaderOption[AppConfig, int] {
|
||||
return func(cfg AppConfig) option.Option[int] {
|
||||
return option.Some(n + len(cfg.LogLevel))
|
||||
}
|
||||
}),
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(14), outcome)
|
||||
})
|
||||
|
||||
t.Run("none", func(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](10),
|
||||
ChainReaderOptionK[AppConfig, int, int](onNone)(func(n int) readeroption.ReaderOption[AppConfig, int] {
|
||||
return func(cfg AppConfig) option.Option[int] {
|
||||
return option.None[int]()
|
||||
}
|
||||
}),
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
})
|
||||
}
|
||||
|
||||
func TestChainIOEitherK(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](21),
|
||||
ChainIOEitherK[AppConfig](func(n int) ioeither.IOEither[error, int] {
|
||||
return ioeither.Of[error](n * 2)
|
||||
}),
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
}
|
||||
|
||||
func TestChainIOK(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](21),
|
||||
ChainIOK[AppConfig](func(n int) io.IO[int] {
|
||||
return func() int { return n * 2 }
|
||||
}),
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
}
|
||||
|
||||
func TestChainOptionK(t *testing.T) {
|
||||
onNone := func() error { return errors.New("none") }
|
||||
|
||||
t.Run("some", func(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](21),
|
||||
ChainOptionK[AppConfig, int, int](onNone)(func(n int) option.Option[int] {
|
||||
return option.Some(n * 2)
|
||||
}),
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
})
|
||||
|
||||
t.Run("none", func(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](21),
|
||||
ChainOptionK[AppConfig, int, int](onNone)(func(n int) option.Option[int] {
|
||||
return option.None[int]()
|
||||
}),
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromReaderIOResult(t *testing.T) {
|
||||
computation := FromReaderIOResult(func(cfg AppConfig) ioresult.IOResult[int] {
|
||||
return func() result.Result[int] {
|
||||
return result.Of(len(cfg.DatabaseURL))
|
||||
}
|
||||
})
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(20), outcome)
|
||||
}
|
||||
|
||||
func TestFromReaderOption(t *testing.T) {
|
||||
onNone := func() error { return errors.New("none") }
|
||||
|
||||
t.Run("some", func(t *testing.T) {
|
||||
computation := FromReaderOption[AppConfig, int](onNone)(func(cfg AppConfig) option.Option[int] {
|
||||
return option.Some(len(cfg.DatabaseURL))
|
||||
})
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(20), outcome)
|
||||
})
|
||||
|
||||
t.Run("none", func(t *testing.T) {
|
||||
computation := FromReaderOption[AppConfig, int](onNone)(func(cfg AppConfig) option.Option[int] {
|
||||
return option.None[int]()
|
||||
})
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadAp(t *testing.T) {
|
||||
fab := Of[AppConfig](N.Mul(2))
|
||||
fa := Of[AppConfig](21)
|
||||
computation := MonadAp(fab, fa)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
}
|
||||
|
||||
func TestAp(t *testing.T) {
|
||||
fa := Of[AppConfig](21)
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](N.Mul(2)),
|
||||
Ap[int](fa),
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
}
|
||||
98
v2/context/readerreaderioresult/retry.go
Normal file
98
v2/context/readerreaderioresult/retry.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package readerreaderioresult
|
||||
|
||||
import (
|
||||
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/retry"
|
||||
)
|
||||
|
||||
// Retrying executes an action with automatic retry logic based on a retry policy.
|
||||
// It retries the action when it fails or when the check predicate returns false.
|
||||
//
|
||||
// This function is useful for handling transient failures in operations like:
|
||||
// - Network requests that may temporarily fail
|
||||
// - Database operations that may encounter locks
|
||||
// - External service calls that may be temporarily unavailable
|
||||
//
|
||||
// Parameters:
|
||||
// - policy: Defines the retry behavior (number of retries, delays, backoff strategy)
|
||||
// - action: The computation to retry, receives retry status information
|
||||
// - check: Predicate to determine if the result should trigger a retry (returns true to continue, false to retry)
|
||||
//
|
||||
// The action receives a retry.RetryStatus that contains:
|
||||
// - IterNumber: Current iteration number (0-based)
|
||||
// - CumulativeDelay: Total delay accumulated so far
|
||||
// - PreviousDelay: Delay from the previous iteration
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderReaderIOResult that executes the action with retry logic
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// "errors"
|
||||
// "time"
|
||||
// "github.com/IBM/fp-go/v2/retry"
|
||||
// )
|
||||
//
|
||||
// type Config struct {
|
||||
// MaxRetries int
|
||||
// BaseDelay time.Duration
|
||||
// }
|
||||
//
|
||||
// // Create a retry policy with exponential backoff
|
||||
// policy := retry.ExponentialBackoff(100*time.Millisecond, 5*time.Second)
|
||||
// policy = retry.LimitRetries(3, policy)
|
||||
//
|
||||
// // Action that may fail transiently
|
||||
// action := func(status retry.RetryStatus) ReaderReaderIOResult[Config, string] {
|
||||
// return func(cfg Config) ReaderIOResult[context.Context, string] {
|
||||
// return func(ctx context.Context) IOResult[string] {
|
||||
// return func() Either[error, string] {
|
||||
// // Simulate transient failure
|
||||
// if status.IterNumber < 2 {
|
||||
// return either.Left[string](errors.New("transient error"))
|
||||
// }
|
||||
// return either.Right[error]("success")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Check if we should retry (retry on any error)
|
||||
// check := func(result Result[string]) bool {
|
||||
// return either.IsRight(result) // Continue only if successful
|
||||
// }
|
||||
//
|
||||
// // Execute with retry logic
|
||||
// result := Retrying(policy, action, check)
|
||||
//
|
||||
//go:inline
|
||||
func Retrying[R, A any](
|
||||
policy retry.RetryPolicy,
|
||||
action Kleisli[R, retry.RetryStatus, A],
|
||||
check Predicate[Result[A]],
|
||||
) ReaderReaderIOResult[R, A] {
|
||||
// get an implementation for the types
|
||||
return F.Flow4(
|
||||
reader.Read[RIOE.ReaderIOResult[A]],
|
||||
reader.Map[retry.RetryStatus],
|
||||
reader.Read[RIOE.Kleisli[retry.RetryStatus, A]](action),
|
||||
F.Bind13of3(RIOE.Retrying[A])(policy, check),
|
||||
)
|
||||
}
|
||||
255
v2/context/readerreaderioresult/retry_test.go
Normal file
255
v2/context/readerreaderioresult/retry_test.go
Normal file
@@ -0,0 +1,255 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerreaderioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/IBM/fp-go/v2/retry"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRetryingSuccess(t *testing.T) {
|
||||
cfg := defaultConfig
|
||||
ctx := t.Context()
|
||||
|
||||
attempts := 0
|
||||
action := func(status retry.RetryStatus) ReaderReaderIOResult[AppConfig, int] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, int] {
|
||||
return func(ctx context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
attempts++
|
||||
if attempts < 3 {
|
||||
return result.Left[int](errors.New("temporary error"))
|
||||
}
|
||||
return result.Of(42)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
check := result.IsLeft[int]
|
||||
|
||||
policy := retry.LimitRetries(5)
|
||||
|
||||
computation := Retrying(policy, action, check)
|
||||
outcome := computation(cfg)(ctx)()
|
||||
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
assert.Equal(t, 3, attempts)
|
||||
}
|
||||
|
||||
func TestRetryingFailureExhaustsRetries(t *testing.T) {
|
||||
cfg := defaultConfig
|
||||
ctx := t.Context()
|
||||
|
||||
attempts := 0
|
||||
testErr := errors.New("persistent error")
|
||||
|
||||
action := func(status retry.RetryStatus) ReaderReaderIOResult[AppConfig, int] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, int] {
|
||||
return func(ctx context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
attempts++
|
||||
return result.Left[int](testErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
check := result.IsLeft[int]
|
||||
|
||||
policy := retry.LimitRetries(3)
|
||||
|
||||
computation := Retrying(policy, action, check)
|
||||
outcome := computation(cfg)(ctx)()
|
||||
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
assert.Equal(t, 4, attempts) // Initial attempt + 3 retries
|
||||
}
|
||||
|
||||
func TestRetryingNoRetryNeeded(t *testing.T) {
|
||||
cfg := defaultConfig
|
||||
ctx := t.Context()
|
||||
|
||||
attempts := 0
|
||||
action := func(status retry.RetryStatus) ReaderReaderIOResult[AppConfig, int] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, int] {
|
||||
return func(ctx context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
attempts++
|
||||
return result.Of(42)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
check := result.IsLeft[int]
|
||||
|
||||
policy := retry.LimitRetries(5)
|
||||
|
||||
computation := Retrying(policy, action, check)
|
||||
outcome := computation(cfg)(ctx)()
|
||||
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
assert.Equal(t, 1, attempts) // Only initial attempt
|
||||
}
|
||||
|
||||
func TestRetryingWithDelay(t *testing.T) {
|
||||
cfg := defaultConfig
|
||||
ctx := t.Context()
|
||||
|
||||
attempts := 0
|
||||
start := time.Now()
|
||||
|
||||
action := func(status retry.RetryStatus) ReaderReaderIOResult[AppConfig, int] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, int] {
|
||||
return func(ctx context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
attempts++
|
||||
if attempts < 2 {
|
||||
return result.Left[int](errors.New("temporary error"))
|
||||
}
|
||||
return result.Of(42)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
check := result.IsLeft[int]
|
||||
|
||||
// Policy with delay
|
||||
policy := retry.CapDelay(
|
||||
100*time.Millisecond,
|
||||
retry.LimitRetries(3),
|
||||
)
|
||||
|
||||
computation := Retrying(policy, action, check)
|
||||
outcome := computation(cfg)(ctx)()
|
||||
elapsed := time.Since(start)
|
||||
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
assert.Equal(t, 2, attempts)
|
||||
// The delay might be very short in tests, so just check it completed
|
||||
_ = elapsed
|
||||
}
|
||||
|
||||
func TestRetryingAccessesConfig(t *testing.T) {
|
||||
cfg := AppConfig{DatabaseURL: "test-db", LogLevel: "debug"}
|
||||
ctx := t.Context()
|
||||
|
||||
attempts := 0
|
||||
var capturedURL string
|
||||
|
||||
action := func(status retry.RetryStatus) ReaderReaderIOResult[AppConfig, int] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, int] {
|
||||
return func(ctx context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
attempts++
|
||||
capturedURL = c.DatabaseURL
|
||||
if attempts < 2 {
|
||||
return result.Left[int](errors.New("temporary error"))
|
||||
}
|
||||
return result.Of(len(c.DatabaseURL))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
check := result.IsLeft[int]
|
||||
|
||||
policy := retry.LimitRetries(3)
|
||||
|
||||
computation := Retrying(policy, action, check)
|
||||
outcome := computation(cfg)(ctx)()
|
||||
|
||||
assert.Equal(t, result.Of(7), outcome) // len("test-db")
|
||||
assert.Equal(t, "test-db", capturedURL)
|
||||
assert.Equal(t, 2, attempts)
|
||||
}
|
||||
|
||||
func TestRetryingWithExponentialBackoff(t *testing.T) {
|
||||
cfg := defaultConfig
|
||||
ctx := t.Context()
|
||||
|
||||
attempts := 0
|
||||
action := func(status retry.RetryStatus) ReaderReaderIOResult[AppConfig, int] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, int] {
|
||||
return func(ctx context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
attempts++
|
||||
if attempts < 3 {
|
||||
return result.Left[int](errors.New("temporary error"))
|
||||
}
|
||||
return result.Of(42)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
check := result.IsLeft[int]
|
||||
|
||||
// Exponential backoff policy
|
||||
policy := retry.CapDelay(
|
||||
200*time.Millisecond,
|
||||
retry.LimitRetries(5),
|
||||
)
|
||||
|
||||
computation := Retrying(policy, action, check)
|
||||
outcome := computation(cfg)(ctx)()
|
||||
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
assert.Equal(t, 3, attempts)
|
||||
}
|
||||
|
||||
func TestRetryingCheckFunction(t *testing.T) {
|
||||
cfg := defaultConfig
|
||||
ctx := t.Context()
|
||||
|
||||
attempts := 0
|
||||
action := func(status retry.RetryStatus) ReaderReaderIOResult[AppConfig, int] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, int] {
|
||||
return func(ctx context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
attempts++
|
||||
return result.Of(attempts)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Retry while result is less than 3
|
||||
check := func(r Result[int]) bool {
|
||||
return result.Fold(
|
||||
reader.Of[error](true),
|
||||
N.LessThan(3),
|
||||
)(r)
|
||||
}
|
||||
|
||||
policy := retry.LimitRetries(10)
|
||||
|
||||
computation := Retrying(policy, action, check)
|
||||
outcome := computation(cfg)(ctx)()
|
||||
|
||||
assert.Equal(t, result.Of(3), outcome)
|
||||
assert.Equal(t, 3, attempts)
|
||||
}
|
||||
154
v2/context/readerreaderioresult/types.go
Normal file
154
v2/context/readerreaderioresult/types.go
Normal file
@@ -0,0 +1,154 @@
|
||||
// Copyright (c) 2024 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 readerreaderioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"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/lazy"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/optics/traversal/result"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
"github.com/IBM/fp-go/v2/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/readeroption"
|
||||
"github.com/IBM/fp-go/v2/readerreaderioeither"
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
|
||||
type (
|
||||
// Option represents an optional value that may or may not be present.
|
||||
// It's an alias for option.Option[A].
|
||||
Option[A any] = option.Option[A]
|
||||
|
||||
// Lazy represents a lazily evaluated computation that produces a value of type A.
|
||||
// It's an alias for lazy.Lazy[A].
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
|
||||
// Reader represents a computation that depends on an environment of type R
|
||||
// and produces a value of type A.
|
||||
// It's an alias for reader.Reader[R, A].
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
|
||||
// ReaderOption represents a computation that depends on an environment of type R
|
||||
// and produces an optional value of type A.
|
||||
// It's an alias for readeroption.ReaderOption[R, A].
|
||||
ReaderOption[R, A any] = readeroption.ReaderOption[R, A]
|
||||
|
||||
// ReaderIO represents a computation that depends on an environment of type R
|
||||
// and performs side effects to produce a value of type A.
|
||||
// It's an alias for readerio.ReaderIO[R, A].
|
||||
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
|
||||
|
||||
// ReaderIOResult represents a computation that depends on an environment of type R,
|
||||
// performs side effects, and may fail with an error.
|
||||
// It's an alias for readerioresult.ReaderIOResult[R, A].
|
||||
ReaderIOResult[R, A any] = readerioresult.ReaderIOResult[R, A]
|
||||
|
||||
// Either represents a value that can be one of two types: Left (error) or Right (success).
|
||||
// It's an alias for either.Either[E, A].
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
|
||||
// Result is a specialized Either with error as the left type.
|
||||
// It's an alias for result.Result[A] which is Either[error, A].
|
||||
Result[A any] = result.Result[A]
|
||||
|
||||
// IOEither represents a side-effecting computation that may fail with an error of type E
|
||||
// or succeed with a value of type A.
|
||||
// It's an alias for ioeither.IOEither[E, A].
|
||||
IOEither[E, A any] = ioeither.IOEither[E, A]
|
||||
|
||||
// IOResult represents a side-effecting computation that may fail with an error
|
||||
// or succeed with a value of type A.
|
||||
// It's an alias for ioresult.IOResult[A] which is IOEither[error, A].
|
||||
IOResult[A any] = ioresult.IOResult[A]
|
||||
|
||||
// IO represents a side-effecting computation that produces a value of type A.
|
||||
// It's an alias for io.IO[A].
|
||||
IO[A any] = io.IO[A]
|
||||
|
||||
// ReaderReaderIOEither is the base monad transformer that combines:
|
||||
// - Reader[R, ...] for outer dependency injection
|
||||
// - Reader[C, ...] for inner dependency injection (typically context.Context)
|
||||
// - IO for side effects
|
||||
// - Either[E, A] for error handling
|
||||
// It's an alias for readerreaderioeither.ReaderReaderIOEither[R, C, E, A].
|
||||
ReaderReaderIOEither[R, C, E, A any] = readerreaderioeither.ReaderReaderIOEither[R, C, E, A]
|
||||
|
||||
// ReaderReaderIOResult is the main type of this package, specializing ReaderReaderIOEither
|
||||
// with context.Context as the inner reader type and error as the error type.
|
||||
//
|
||||
// Type structure:
|
||||
// ReaderReaderIOResult[R, A] = R -> context.Context -> IO[Either[error, A]]
|
||||
//
|
||||
// This represents a computation that:
|
||||
// 1. Depends on an outer environment of type R (e.g., application config)
|
||||
// 2. Depends on a context.Context for cancellation and request-scoped values
|
||||
// 3. Performs side effects (IO)
|
||||
// 4. May fail with an error or succeed with a value of type A
|
||||
//
|
||||
// This is the primary type used throughout the package for composing
|
||||
// context-aware, effectful computations with error handling.
|
||||
ReaderReaderIOResult[R, A any] = ReaderReaderIOEither[R, context.Context, error, A]
|
||||
|
||||
// Kleisli represents a function from A to a monadic value ReaderReaderIOResult[R, B].
|
||||
// It's used for composing monadic functions using Kleisli composition.
|
||||
//
|
||||
// Type structure:
|
||||
// Kleisli[R, A, B] = A -> ReaderReaderIOResult[R, B]
|
||||
//
|
||||
// Kleisli arrows can be composed using Chain operations to build complex
|
||||
// data transformation pipelines.
|
||||
Kleisli[R, A, B any] = Reader[A, ReaderReaderIOResult[R, B]]
|
||||
|
||||
// Operator is a specialized Kleisli arrow that operates on monadic values.
|
||||
// It takes a ReaderReaderIOResult[R, A] and produces a ReaderReaderIOResult[R, B].
|
||||
//
|
||||
// Type structure:
|
||||
// Operator[R, A, B] = ReaderReaderIOResult[R, A] -> ReaderReaderIOResult[R, B]
|
||||
//
|
||||
// Operators are useful for transforming monadic computations, such as
|
||||
// adding retry logic, logging, or error recovery.
|
||||
Operator[R, A, B any] = Kleisli[R, ReaderReaderIOResult[R, A], B]
|
||||
|
||||
// Lens represents an optic for focusing on a part of a data structure.
|
||||
// It provides a way to get and set a field T within a structure S.
|
||||
// It's an alias for lens.Lens[S, T].
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
|
||||
// Trampoline is used for stack-safe recursion through tail call optimization.
|
||||
// It's an alias for tailrec.Trampoline[L, B].
|
||||
Trampoline[L, B any] = tailrec.Trampoline[L, B]
|
||||
|
||||
// Predicate represents a function that tests whether a value of type A
|
||||
// satisfies some condition.
|
||||
// It's an alias for predicate.Predicate[A].
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
|
||||
// Endmorphism represents a function from type A to type A.
|
||||
// It's an alias for endomorphism.Endomorphism[A].
|
||||
Endmorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
Void = function.Void
|
||||
)
|
||||
@@ -58,7 +58,7 @@ import (
|
||||
// safeLongComputation := readerresult.WithContext(longComputation)
|
||||
//
|
||||
// // Cancel the context before execution
|
||||
// ctx, cancel := context.WithCancel(context.Background())
|
||||
// ctx, cancel := context.WithCancel(t.Context())
|
||||
// cancel()
|
||||
//
|
||||
// // The computation returns immediately with cancellation error
|
||||
@@ -76,7 +76,7 @@ import (
|
||||
// safeFetch := readerresult.WithContext(fetchData)
|
||||
//
|
||||
// // Context with 1 second timeout
|
||||
// ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
// ctx, cancel := context.WithTimeout(t.Context(), 1*time.Second)
|
||||
// defer cancel()
|
||||
//
|
||||
// time.Sleep(1500 * time.Millisecond) // Wait for timeout
|
||||
@@ -139,7 +139,7 @@ func WithContext[A any](ma ReaderResult[A]) ReaderResult[A] {
|
||||
// )
|
||||
//
|
||||
// // If context is cancelled, processUser never executes
|
||||
// ctx, cancel := context.WithCancel(context.Background())
|
||||
// ctx, cancel := context.WithCancel(t.Context())
|
||||
// cancel()
|
||||
// result := pipeline(ctx) // Left(context.Canceled)
|
||||
//
|
||||
@@ -165,7 +165,7 @@ func WithContext[A any](ma ReaderResult[A]) ReaderResult[A] {
|
||||
// )
|
||||
//
|
||||
// // If context is cancelled at any point, remaining steps don't execute
|
||||
// ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
// ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
|
||||
// defer cancel()
|
||||
// result := pipeline(ctx)
|
||||
//
|
||||
|
||||
@@ -38,7 +38,7 @@ func TestWithContext(t *testing.T) {
|
||||
}
|
||||
|
||||
wrapped := WithContext(computation)
|
||||
result := wrapped(context.Background())
|
||||
result := wrapped(t.Context())
|
||||
|
||||
assert.True(t, executed, "computation should be executed")
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
@@ -53,7 +53,7 @@ func TestWithContext(t *testing.T) {
|
||||
|
||||
wrapped := WithContext(computation)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
result := wrapped(ctx)
|
||||
@@ -74,7 +74,7 @@ func TestWithContext(t *testing.T) {
|
||||
|
||||
wrapped := WithContext(computation)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
time.Sleep(20 * time.Millisecond) // Wait for timeout
|
||||
@@ -94,7 +94,7 @@ func TestWithContext(t *testing.T) {
|
||||
}
|
||||
|
||||
wrapped := WithContext(computation)
|
||||
result := wrapped(context.Background())
|
||||
result := wrapped(t.Context())
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, err := E.UnwrapError(result)
|
||||
@@ -112,7 +112,7 @@ func TestWithContext(t *testing.T) {
|
||||
|
||||
wrapped := WithContext(expensiveComputation)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
start := time.Now()
|
||||
@@ -134,7 +134,7 @@ func TestWithContext(t *testing.T) {
|
||||
wrapped := WithContext(computation)
|
||||
|
||||
customErr := errors.New("custom cancellation reason")
|
||||
ctx, cancel := context.WithCancelCause(context.Background())
|
||||
ctx, cancel := context.WithCancelCause(t.Context())
|
||||
cancel(customErr)
|
||||
|
||||
result := wrapped(ctx)
|
||||
@@ -154,7 +154,7 @@ func TestWithContext(t *testing.T) {
|
||||
|
||||
doubleWrapped := WithContext(WithContext(computation))
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
result := doubleWrapped(ctx)
|
||||
@@ -177,7 +177,7 @@ func TestWithContextK(t *testing.T) {
|
||||
|
||||
safeProcessUser := WithContextK(processUser)
|
||||
|
||||
result := safeProcessUser(123)(context.Background())
|
||||
result := safeProcessUser(123)(t.Context())
|
||||
|
||||
assert.True(t, executed, "Kleisli should be executed")
|
||||
assert.True(t, E.IsRight(result))
|
||||
@@ -194,7 +194,7 @@ func TestWithContextK(t *testing.T) {
|
||||
|
||||
safeProcessUser := WithContextK(processUser)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
result := safeProcessUser(123)(ctx)
|
||||
@@ -229,7 +229,7 @@ func TestWithContextK(t *testing.T) {
|
||||
Chain(getOrders),
|
||||
)
|
||||
|
||||
result := pipeline(context.Background())
|
||||
result := pipeline(t.Context())
|
||||
|
||||
assert.True(t, firstExecuted, "first step should execute")
|
||||
assert.True(t, secondExecuted, "second step should execute")
|
||||
@@ -260,7 +260,7 @@ func TestWithContextK(t *testing.T) {
|
||||
Chain(getOrders),
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
result := pipeline(ctx)
|
||||
@@ -295,7 +295,7 @@ func TestWithContextK(t *testing.T) {
|
||||
Chain(step2),
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
time.Sleep(20 * time.Millisecond) // Wait for timeout
|
||||
@@ -316,7 +316,7 @@ func TestWithContextK(t *testing.T) {
|
||||
}
|
||||
|
||||
safeKleisli := WithContextK(failingKleisli)
|
||||
result := safeKleisli(123)(context.Background())
|
||||
result := safeKleisli(123)(t.Context())
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, err := E.UnwrapError(result)
|
||||
@@ -354,7 +354,7 @@ func TestWithContextIntegration(t *testing.T) {
|
||||
ChainTo[int](step3),
|
||||
)
|
||||
|
||||
result := pipeline(context.Background())
|
||||
result := pipeline(t.Context())
|
||||
|
||||
assert.True(t, step1Executed)
|
||||
assert.True(t, step2Executed)
|
||||
@@ -390,7 +390,7 @@ func TestWithContextIntegration(t *testing.T) {
|
||||
ChainTo[int](step3),
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
result := pipeline(ctx)
|
||||
@@ -412,7 +412,7 @@ func TestWithContextIntegration(t *testing.T) {
|
||||
Map(reader.Of[int]("result")),
|
||||
)
|
||||
|
||||
result := pipeline(context.Background())
|
||||
result := pipeline(t.Context())
|
||||
assert.Equal(t, E.Of[error]("result"), result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ import (
|
||||
//
|
||||
// rr := readerresult.Of(42)
|
||||
// tupled := readerresult.SequenceT1(rr)
|
||||
// result := tupled(context.Background())
|
||||
// result := tupled(t.Context())
|
||||
// // result is Right(Tuple1{F1: 42})
|
||||
//
|
||||
//go:inline
|
||||
@@ -81,7 +81,7 @@ func SequenceT1[A any](a ReaderResult[A]) ReaderResult[tuple.Tuple1[A]] {
|
||||
// getName := readerresult.Of("Alice")
|
||||
// getAge := readerresult.Of(30)
|
||||
// combined := readerresult.SequenceT2(getName, getAge)
|
||||
// result := combined(context.Background())
|
||||
// result := combined(t.Context())
|
||||
// // result is Right(Tuple2{F1: "Alice", F2: 30})
|
||||
//
|
||||
// Example with error:
|
||||
@@ -89,7 +89,7 @@ func SequenceT1[A any](a ReaderResult[A]) ReaderResult[tuple.Tuple1[A]] {
|
||||
// getName := readerresult.Of("Alice")
|
||||
// getAge := readerresult.Left[int](errors.New("age not found"))
|
||||
// combined := readerresult.SequenceT2(getName, getAge)
|
||||
// result := combined(context.Background())
|
||||
// result := combined(t.Context())
|
||||
// // result is Left(error("age not found"))
|
||||
//
|
||||
//go:inline
|
||||
@@ -120,7 +120,7 @@ func SequenceT2[A, B any](a ReaderResult[A], b ReaderResult[B]) ReaderResult[tup
|
||||
// getUserName := readerresult.Of("Alice")
|
||||
// getUserEmail := readerresult.Of("alice@example.com")
|
||||
// combined := readerresult.SequenceT3(getUserID, getUserName, getUserEmail)
|
||||
// result := combined(context.Background())
|
||||
// result := combined(t.Context())
|
||||
// // result is Right(Tuple3{F1: 123, F2: "Alice", F3: "alice@example.com"})
|
||||
//
|
||||
// Example with context-aware operations:
|
||||
@@ -138,7 +138,7 @@ func SequenceT2[A, B any](a ReaderResult[A], b ReaderResult[B]) ReaderResult[tup
|
||||
// return result.Of("NYC")
|
||||
// }
|
||||
// combined := readerresult.SequenceT3(fetchUser, fetchAge, fetchCity)
|
||||
// result := combined(context.Background())
|
||||
// result := combined(t.Context())
|
||||
// // result is Right(Tuple3{F1: "Alice", F2: 30, F3: "NYC"})
|
||||
//
|
||||
//go:inline
|
||||
@@ -172,7 +172,7 @@ func SequenceT3[A, B, C any](a ReaderResult[A], b ReaderResult[B], c ReaderResul
|
||||
// getEmail := readerresult.Of("alice@example.com")
|
||||
// getAge := readerresult.Of(30)
|
||||
// combined := readerresult.SequenceT4(getID, getName, getEmail, getAge)
|
||||
// result := combined(context.Background())
|
||||
// result := combined(t.Context())
|
||||
// // result is Right(Tuple4{F1: 123, F2: "Alice", F3: "alice@example.com", F4: 30})
|
||||
//
|
||||
// Example with early failure:
|
||||
@@ -182,7 +182,7 @@ func SequenceT3[A, B, C any](a ReaderResult[A], b ReaderResult[B], c ReaderResul
|
||||
// getEmail := readerresult.Of("alice@example.com") // Not executed
|
||||
// getAge := readerresult.Of(30) // Not executed
|
||||
// combined := readerresult.SequenceT4(getID, getName, getEmail, getAge)
|
||||
// result := combined(context.Background())
|
||||
// result := combined(t.Context())
|
||||
// // result is Left(error("name not found"))
|
||||
// // getEmail and getAge are never executed due to early failure
|
||||
//
|
||||
@@ -212,7 +212,7 @@ func SequenceT3[A, B, C any](a ReaderResult[A], b ReaderResult[B], c ReaderResul
|
||||
// })
|
||||
//
|
||||
// userProfile := F.Pipe1(fetchUserData, buildProfile)
|
||||
// result := userProfile(context.Background())
|
||||
// result := userProfile(t.Context())
|
||||
//
|
||||
//go:inline
|
||||
func SequenceT4[A, B, C, D any](a ReaderResult[A], b ReaderResult[B], c ReaderResult[C], d ReaderResult[D]) ReaderResult[tuple.Tuple4[A, B, C, D]] {
|
||||
|
||||
@@ -30,7 +30,7 @@ func TestSequenceT1(t *testing.T) {
|
||||
t.Run("wraps single success value in tuple", func(t *testing.T) {
|
||||
rr := Of(42)
|
||||
tupled := SequenceT1(rr)
|
||||
result := tupled(context.Background())
|
||||
result := tupled(t.Context())
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
val, _ := E.Unwrap(result)
|
||||
@@ -41,7 +41,7 @@ func TestSequenceT1(t *testing.T) {
|
||||
testErr := errors.New("test error")
|
||||
rr := Left[int](testErr)
|
||||
tupled := SequenceT1(rr)
|
||||
result := tupled(context.Background())
|
||||
result := tupled(t.Context())
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, err := E.UnwrapError(result)
|
||||
@@ -58,7 +58,7 @@ func TestSequenceT1(t *testing.T) {
|
||||
|
||||
tupled := SequenceT1(rr)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
result := tupled(ctx)
|
||||
@@ -73,7 +73,7 @@ func TestSequenceT2(t *testing.T) {
|
||||
getAge := Of(30)
|
||||
|
||||
combined := SequenceT2(getName, getAge)
|
||||
result := combined(context.Background())
|
||||
result := combined(t.Context())
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
val, _ := E.Unwrap(result)
|
||||
@@ -87,7 +87,7 @@ func TestSequenceT2(t *testing.T) {
|
||||
getAge := Of(30)
|
||||
|
||||
combined := SequenceT2(getName, getAge)
|
||||
result := combined(context.Background())
|
||||
result := combined(t.Context())
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, err := E.UnwrapError(result)
|
||||
@@ -100,7 +100,7 @@ func TestSequenceT2(t *testing.T) {
|
||||
getAge := Left[int](testErr)
|
||||
|
||||
combined := SequenceT2(getName, getAge)
|
||||
result := combined(context.Background())
|
||||
result := combined(t.Context())
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, err := E.UnwrapError(result)
|
||||
@@ -109,7 +109,7 @@ func TestSequenceT2(t *testing.T) {
|
||||
|
||||
t.Run("executes both ReaderResults with same context", func(t *testing.T) {
|
||||
type ctxKey string
|
||||
ctx := context.WithValue(context.Background(), ctxKey("key"), "shared")
|
||||
ctx := context.WithValue(t.Context(), ctxKey("key"), "shared")
|
||||
|
||||
getName := func(ctx context.Context) E.Either[error, string] {
|
||||
val := ctx.Value(ctxKey("key"))
|
||||
@@ -151,7 +151,7 @@ func TestSequenceT2(t *testing.T) {
|
||||
}
|
||||
|
||||
combined := SequenceT2(first, second)
|
||||
result := combined(context.Background())
|
||||
result := combined(t.Context())
|
||||
|
||||
assert.True(t, firstExecuted, "first should be executed")
|
||||
assert.True(t, secondExecuted, "second should be executed (applicative semantics)")
|
||||
@@ -167,7 +167,7 @@ func TestSequenceT3(t *testing.T) {
|
||||
getUserEmail := Of("alice@example.com")
|
||||
|
||||
combined := SequenceT3(getUserID, getUserName, getUserEmail)
|
||||
result := combined(context.Background())
|
||||
result := combined(t.Context())
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
val, _ := E.Unwrap(result)
|
||||
@@ -183,7 +183,7 @@ func TestSequenceT3(t *testing.T) {
|
||||
getUserEmail := Left[string](testErr)
|
||||
|
||||
combined := SequenceT3(getUserID, getUserName, getUserEmail)
|
||||
result := combined(context.Background())
|
||||
result := combined(t.Context())
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, err := E.UnwrapError(result)
|
||||
@@ -211,7 +211,7 @@ func TestSequenceT3(t *testing.T) {
|
||||
}
|
||||
|
||||
combined := SequenceT3(first, second, third)
|
||||
result := combined(context.Background())
|
||||
result := combined(t.Context())
|
||||
|
||||
assert.True(t, firstExecuted, "first should be executed")
|
||||
assert.True(t, secondExecuted, "second should be executed")
|
||||
@@ -232,7 +232,7 @@ func TestSequenceT3(t *testing.T) {
|
||||
|
||||
combined := SequenceT3(getUserID, getUserName, getUserEmail)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
result := combined(ctx)
|
||||
@@ -249,7 +249,7 @@ func TestSequenceT4(t *testing.T) {
|
||||
getAge := Of(30)
|
||||
|
||||
combined := SequenceT4(getID, getName, getEmail, getAge)
|
||||
result := combined(context.Background())
|
||||
result := combined(t.Context())
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
val, _ := E.Unwrap(result)
|
||||
@@ -267,7 +267,7 @@ func TestSequenceT4(t *testing.T) {
|
||||
getAge := Of(30)
|
||||
|
||||
combined := SequenceT4(getID, getName, getEmail, getAge)
|
||||
result := combined(context.Background())
|
||||
result := combined(t.Context())
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, err := E.UnwrapError(result)
|
||||
@@ -301,7 +301,7 @@ func TestSequenceT4(t *testing.T) {
|
||||
}
|
||||
|
||||
combined := SequenceT4(first, second, third, fourth)
|
||||
result := combined(context.Background())
|
||||
result := combined(t.Context())
|
||||
|
||||
assert.True(t, firstExecuted, "first should be executed")
|
||||
assert.True(t, secondExecuted, "second should be executed")
|
||||
@@ -344,7 +344,7 @@ func TestSequenceT4(t *testing.T) {
|
||||
return buildProfile(Of(tupleVal))(ctx)
|
||||
}
|
||||
|
||||
result := userProfile(context.Background())
|
||||
result := userProfile(t.Context())
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
profile, _ := E.Unwrap(result)
|
||||
@@ -356,7 +356,7 @@ func TestSequenceT4(t *testing.T) {
|
||||
|
||||
t.Run("executes all with same context", func(t *testing.T) {
|
||||
type ctxKey string
|
||||
ctx := context.WithValue(context.Background(), ctxKey("multiplier"), 2)
|
||||
ctx := context.WithValue(t.Context(), ctxKey("multiplier"), 2)
|
||||
|
||||
getBase := func(ctx context.Context) E.Either[error, int] {
|
||||
return E.Of[error](10)
|
||||
@@ -409,7 +409,7 @@ func TestSequenceIntegration(t *testing.T) {
|
||||
return formatted(Of(tupleVal))(ctx)
|
||||
}
|
||||
|
||||
result := pipeline(context.Background())
|
||||
result := pipeline(t.Context())
|
||||
assert.True(t, E.IsRight(result))
|
||||
})
|
||||
|
||||
@@ -434,7 +434,7 @@ func TestSequenceIntegration(t *testing.T) {
|
||||
return sumTuple(tupleVal)(ctx)
|
||||
}
|
||||
|
||||
result := pipeline(context.Background())
|
||||
result := pipeline(t.Context())
|
||||
assert.True(t, E.IsRight(result))
|
||||
val, _ := E.Unwrap(result)
|
||||
assert.Equal(t, 60, val) // 10 + 20 + 30
|
||||
@@ -448,7 +448,7 @@ func TestSequenceIntegration(t *testing.T) {
|
||||
// Combine the pairs
|
||||
combined := SequenceT2(pair1, pair2)
|
||||
|
||||
result := combined(context.Background())
|
||||
result := combined(t.Context())
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
val, _ := E.Unwrap(result)
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
//
|
||||
// // Execute with initial state and context
|
||||
// initialState := AppState{RequestCount: 0}
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// outcome := result(initialState)(ctx)() // Returns result.Result[pair.Pair[AppState, string]]
|
||||
//
|
||||
// # Context Integration
|
||||
|
||||
@@ -47,7 +47,7 @@ import (
|
||||
// onNegative := func(n int) error { return fmt.Errorf("%d is not positive", n) }
|
||||
//
|
||||
// filter := statereaderioresult.FilterOrElse[AppState](isPositive, onNegative)
|
||||
// result := filter(statereaderioresult.Right[AppState](42))(AppState{})(context.Background())()
|
||||
// result := filter(statereaderioresult.Right[AppState](42))(AppState{})(t.Context())()
|
||||
//
|
||||
//go:inline
|
||||
func FilterOrElse[S, A any](pred Predicate[A], onFalse func(A) error) Operator[S, A, A] {
|
||||
|
||||
@@ -91,7 +91,7 @@ import "github.com/IBM/fp-go/v2/statereaderioeither"
|
||||
//
|
||||
// // Execute the computation
|
||||
// initialState := AppState{openFiles: 0}
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// outcome := result(initialState)(ctx)()
|
||||
func WithResource[A, S, RES, ANY any](
|
||||
onCreate StateReaderIOResult[S, RES],
|
||||
|
||||
@@ -41,7 +41,7 @@ type mockResource struct {
|
||||
// TestWithResourceSuccess tests successful resource creation, usage, and release
|
||||
func TestWithResourceSuccess(t *testing.T) {
|
||||
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Create a resource
|
||||
onCreate := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] {
|
||||
@@ -110,7 +110,7 @@ func TestWithResourceSuccess(t *testing.T) {
|
||||
// TestWithResourceErrorInCreate tests error handling when resource creation fails
|
||||
func TestWithResourceErrorInCreate(t *testing.T) {
|
||||
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
createError := errors.New("failed to create resource")
|
||||
|
||||
@@ -159,7 +159,7 @@ func TestWithResourceErrorInCreate(t *testing.T) {
|
||||
// TestWithResourceErrorInUse tests that resources are released even when usage fails
|
||||
func TestWithResourceErrorInUse(t *testing.T) {
|
||||
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
useError := errors.New("failed to use resource")
|
||||
|
||||
@@ -222,7 +222,7 @@ func TestWithResourceErrorInUse(t *testing.T) {
|
||||
// TestWithResourceStateThreading tests that state is properly threaded through all operations
|
||||
func TestWithResourceStateThreading(t *testing.T) {
|
||||
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Create increments counter
|
||||
onCreate := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] {
|
||||
@@ -295,7 +295,7 @@ func TestWithResourceStateThreading(t *testing.T) {
|
||||
// TestWithResourceMultipleResources tests using WithResource multiple times (nesting)
|
||||
func TestWithResourceMultipleResources(t *testing.T) {
|
||||
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
createResource := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] {
|
||||
return func(ctx context.Context) IOResult[Pair[resourceState, mockResource]] {
|
||||
@@ -357,7 +357,7 @@ func TestWithResourceMultipleResources(t *testing.T) {
|
||||
// TestWithResourceContextCancellation tests behavior with context cancellation
|
||||
func TestWithResourceContextCancellation(t *testing.T) {
|
||||
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel() // Cancel immediately
|
||||
|
||||
cancelError := errors.New("context cancelled")
|
||||
|
||||
@@ -36,7 +36,7 @@ type testState struct {
|
||||
|
||||
func TestOf(t *testing.T) {
|
||||
state := testState{counter: 0}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
result := Of[testState](42)
|
||||
res := result(state)(ctx)()
|
||||
|
||||
@@ -56,7 +56,7 @@ func TestOf(t *testing.T) {
|
||||
|
||||
func TestRight(t *testing.T) {
|
||||
state := testState{counter: 5}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
result := Right[testState](100)
|
||||
res := result(state)(ctx)()
|
||||
|
||||
@@ -70,7 +70,7 @@ func TestRight(t *testing.T) {
|
||||
|
||||
func TestLeft(t *testing.T) {
|
||||
state := testState{counter: 10}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
testErr := errors.New("test error")
|
||||
result := Left[testState, int](testErr)
|
||||
res := result(state)(ctx)()
|
||||
@@ -80,7 +80,7 @@ func TestLeft(t *testing.T) {
|
||||
|
||||
func TestMonadMap(t *testing.T) {
|
||||
state := testState{counter: 0}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
result := MonadMap(
|
||||
Of[testState](21),
|
||||
@@ -97,7 +97,7 @@ func TestMonadMap(t *testing.T) {
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
state := testState{counter: 0}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
result := F.Pipe1(
|
||||
Of[testState](21),
|
||||
@@ -114,7 +114,7 @@ func TestMap(t *testing.T) {
|
||||
|
||||
func TestMonadChain(t *testing.T) {
|
||||
state := testState{counter: 0}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
result := MonadChain(
|
||||
Of[testState](5),
|
||||
@@ -133,7 +133,7 @@ func TestMonadChain(t *testing.T) {
|
||||
|
||||
func TestChain(t *testing.T) {
|
||||
state := testState{counter: 0}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
result := F.Pipe1(
|
||||
Of[testState](5),
|
||||
@@ -152,7 +152,7 @@ func TestChain(t *testing.T) {
|
||||
|
||||
func TestMonadAp(t *testing.T) {
|
||||
state := testState{counter: 0}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
fab := Of[testState](N.Mul(2))
|
||||
fa := Of[testState](21)
|
||||
@@ -168,7 +168,7 @@ func TestMonadAp(t *testing.T) {
|
||||
|
||||
func TestAp(t *testing.T) {
|
||||
state := testState{counter: 0}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
fa := Of[testState](21)
|
||||
result := F.Pipe1(
|
||||
@@ -186,7 +186,7 @@ func TestAp(t *testing.T) {
|
||||
|
||||
func TestFromIOResult(t *testing.T) {
|
||||
state := testState{counter: 3}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
ior := IOR.Of(55)
|
||||
result := FromIOResult[testState](ior)
|
||||
@@ -202,7 +202,7 @@ func TestFromIOResult(t *testing.T) {
|
||||
|
||||
func TestFromState(t *testing.T) {
|
||||
initialState := testState{counter: 10}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// State computation that increments counter and returns it
|
||||
stateComp := func(s testState) P.Pair[testState, int] {
|
||||
@@ -223,7 +223,7 @@ func TestFromState(t *testing.T) {
|
||||
|
||||
func TestFromIO(t *testing.T) {
|
||||
state := testState{counter: 8}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
ioVal := func() int { return 99 }
|
||||
result := FromIO[testState](ioVal)
|
||||
@@ -239,7 +239,7 @@ func TestFromIO(t *testing.T) {
|
||||
|
||||
func TestFromResult(t *testing.T) {
|
||||
state := testState{counter: 12}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test Success case
|
||||
resultSuccess := FromResult[testState](RES.Of(42))
|
||||
@@ -254,7 +254,7 @@ func TestFromResult(t *testing.T) {
|
||||
|
||||
func TestLocal(t *testing.T) {
|
||||
state := testState{counter: 0}
|
||||
ctx := context.WithValue(context.Background(), "key", "value1")
|
||||
ctx := context.WithValue(t.Context(), "key", "value1")
|
||||
|
||||
// Create a computation that uses the context
|
||||
comp := Asks(func(c context.Context) StateReaderIOResult[testState, string] {
|
||||
@@ -279,7 +279,7 @@ func TestLocal(t *testing.T) {
|
||||
|
||||
func TestAsks(t *testing.T) {
|
||||
state := testState{counter: 0}
|
||||
ctx := context.WithValue(context.Background(), "multiplier", 7)
|
||||
ctx := context.WithValue(t.Context(), "multiplier", 7)
|
||||
|
||||
result := Asks(func(c context.Context) StateReaderIOResult[testState, int] {
|
||||
mult := c.Value("multiplier").(int)
|
||||
@@ -296,7 +296,7 @@ func TestAsks(t *testing.T) {
|
||||
|
||||
func TestFromResultK(t *testing.T) {
|
||||
state := testState{counter: 0}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
validate := func(x int) RES.Result[int] {
|
||||
if x > 0 {
|
||||
@@ -324,7 +324,7 @@ func TestFromResultK(t *testing.T) {
|
||||
|
||||
func TestFromIOK(t *testing.T) {
|
||||
state := testState{counter: 0}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
ioFunc := func(x int) io.IO[int] {
|
||||
return func() int { return x * 3 }
|
||||
@@ -343,7 +343,7 @@ func TestFromIOK(t *testing.T) {
|
||||
|
||||
func TestFromIOResultK(t *testing.T) {
|
||||
state := testState{counter: 0}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
iorFunc := func(x int) IOR.IOResult[int] {
|
||||
if x > 0 {
|
||||
@@ -365,7 +365,7 @@ func TestFromIOResultK(t *testing.T) {
|
||||
|
||||
func TestChainResultK(t *testing.T) {
|
||||
state := testState{counter: 0}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
validate := func(x int) RES.Result[string] {
|
||||
if x > 0 {
|
||||
@@ -389,7 +389,7 @@ func TestChainResultK(t *testing.T) {
|
||||
|
||||
func TestChainIOResultK(t *testing.T) {
|
||||
state := testState{counter: 0}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
iorFunc := func(x int) IOR.IOResult[string] {
|
||||
return IOR.Of(fmt.Sprintf("result: %d", x))
|
||||
@@ -410,7 +410,7 @@ func TestChainIOResultK(t *testing.T) {
|
||||
|
||||
func TestDo(t *testing.T) {
|
||||
state := testState{counter: 0}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
type Result struct {
|
||||
value int
|
||||
@@ -428,7 +428,7 @@ func TestDo(t *testing.T) {
|
||||
|
||||
func TestBindTo(t *testing.T) {
|
||||
state := testState{counter: 0}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
type Result struct {
|
||||
value int
|
||||
@@ -451,7 +451,7 @@ func TestBindTo(t *testing.T) {
|
||||
|
||||
func TestStatefulComputation(t *testing.T) {
|
||||
initialState := testState{counter: 0}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Create a computation that modifies state
|
||||
incrementAndGet := func(s testState) P.Pair[testState, int] {
|
||||
@@ -481,7 +481,7 @@ func TestStatefulComputation(t *testing.T) {
|
||||
|
||||
func TestErrorPropagation(t *testing.T) {
|
||||
state := testState{counter: 0}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
testErr := errors.New("test error")
|
||||
|
||||
@@ -503,7 +503,7 @@ func TestPointed(t *testing.T) {
|
||||
|
||||
result := p.Of(42)
|
||||
state := testState{counter: 0}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
res := result(state)(ctx)()
|
||||
|
||||
assert.True(t, RES.IsRight(res))
|
||||
@@ -517,7 +517,7 @@ func TestFunctor(t *testing.T) {
|
||||
result := mapper(Of[testState](42))
|
||||
|
||||
state := testState{counter: 0}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
res := result(state)(ctx)()
|
||||
|
||||
assert.True(t, RES.IsRight(res))
|
||||
@@ -536,7 +536,7 @@ func TestApplicative(t *testing.T) {
|
||||
result := a.Ap(fa)(fab)
|
||||
|
||||
state := testState{counter: 0}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
res := result(state)(ctx)()
|
||||
|
||||
assert.True(t, RES.IsRight(res))
|
||||
@@ -556,7 +556,7 @@ func TestMonad(t *testing.T) {
|
||||
})(fa)
|
||||
|
||||
state := testState{counter: 0}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
res := result(state)(ctx)()
|
||||
|
||||
assert.True(t, RES.IsRight(res))
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
package testing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
@@ -43,7 +42,7 @@ func TestMonadLaws(t *testing.T) {
|
||||
return fmt.Sprintf("value %d", b)
|
||||
}
|
||||
|
||||
laws := AssertLaws(t, eqs, eqa, eqb, eqc, ab, bc, A.Empty[string](), context.Background())
|
||||
laws := AssertLaws(t, eqs, eqa, eqb, eqc, ab, bc, A.Empty[string](), t.Context())
|
||||
|
||||
assert.True(t, laws(true))
|
||||
assert.True(t, laws(false))
|
||||
|
||||
@@ -169,7 +169,7 @@ func TestContramapMemoize(t *testing.T) {
|
||||
}
|
||||
|
||||
// Cache by ID only
|
||||
cacheByID := ContramapMemoize[string, User, int](func(u User) int {
|
||||
cacheByID := ContramapMemoize[string](func(u User) int {
|
||||
return u.ID
|
||||
})
|
||||
|
||||
@@ -206,7 +206,7 @@ func TestContramapMemoize(t *testing.T) {
|
||||
return p.Price * 1.1 // Add 10% markup
|
||||
}
|
||||
|
||||
cacheBySKU := ContramapMemoize[float64, Product, string](func(p Product) string {
|
||||
cacheBySKU := ContramapMemoize[float64](func(p Product) string {
|
||||
return p.SKU
|
||||
})
|
||||
|
||||
@@ -238,7 +238,7 @@ func TestContramapMemoize(t *testing.T) {
|
||||
}
|
||||
|
||||
// Cache by method and path, ignore body
|
||||
cacheByMethodPath := ContramapMemoize[string, Request, string](func(r Request) string {
|
||||
cacheByMethodPath := ContramapMemoize[string](func(r Request) string {
|
||||
return r.Method + ":" + r.Path
|
||||
})
|
||||
|
||||
@@ -300,7 +300,7 @@ func TestCacheCallback(t *testing.T) {
|
||||
return fmt.Sprintf("Result: %d", n)
|
||||
}
|
||||
|
||||
memoizer := CacheCallback[string, int, int](
|
||||
memoizer := CacheCallback(
|
||||
Identity[int],
|
||||
boundedCache(),
|
||||
)
|
||||
@@ -372,7 +372,7 @@ func TestCacheCallback(t *testing.T) {
|
||||
return fmt.Sprintf("Processed: %s", item.Value)
|
||||
}
|
||||
|
||||
memoizer := CacheCallback[string, Item, int](
|
||||
memoizer := CacheCallback(
|
||||
func(item Item) int { return item.ID },
|
||||
simpleCache(),
|
||||
)
|
||||
@@ -445,7 +445,7 @@ func TestSingleElementCache(t *testing.T) {
|
||||
return fmt.Sprintf("Result: %d", n*n)
|
||||
}
|
||||
|
||||
memoizer := CacheCallback[string, int, int](
|
||||
memoizer := CacheCallback(
|
||||
Identity[int],
|
||||
cache,
|
||||
)
|
||||
@@ -591,7 +591,7 @@ func TestMemoizeIntegration(t *testing.T) {
|
||||
}
|
||||
|
||||
// First level: cache by UserID
|
||||
cacheByUser := ContramapMemoize[string, Request, int](func(r Request) int {
|
||||
cacheByUser := ContramapMemoize[string](func(r Request) int {
|
||||
return r.UserID
|
||||
})
|
||||
|
||||
|
||||
@@ -253,7 +253,7 @@ func Second[T1, T2 any](_ T1, t2 T2) T2 {
|
||||
}
|
||||
|
||||
// Zero returns the zero value of the given type.
|
||||
func Zero[A comparable]() A {
|
||||
func Zero[A any]() A {
|
||||
var zero A
|
||||
return zero
|
||||
}
|
||||
|
||||
@@ -4,14 +4,11 @@ go 1.24
|
||||
|
||||
require (
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/urfave/cli/v2 v2.27.7
|
||||
github.com/urfave/cli/v3 v3.6.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
10
v2/go.sum
10
v2/go.sum
@@ -1,17 +1,11 @@
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
|
||||
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8=
|
||||
github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -204,7 +204,7 @@ func BenchmarkMonadChain_Left(b *testing.B) {
|
||||
|
||||
func BenchmarkChain_Right(b *testing.B) {
|
||||
rioe := Right[benchConfig](42)
|
||||
chainer := Chain[benchConfig](func(a int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](a * 2) })
|
||||
chainer := Chain(func(a int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](a * 2) })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
@@ -214,7 +214,7 @@ func BenchmarkChain_Right(b *testing.B) {
|
||||
|
||||
func BenchmarkChain_Left(b *testing.B) {
|
||||
rioe := Left[benchConfig, int](benchErr)
|
||||
chainer := Chain[benchConfig](func(a int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](a * 2) })
|
||||
chainer := Chain(func(a int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](a * 2) })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
@@ -224,7 +224,7 @@ func BenchmarkChain_Left(b *testing.B) {
|
||||
|
||||
func BenchmarkChainFirst_Right(b *testing.B) {
|
||||
rioe := Right[benchConfig](42)
|
||||
chainer := ChainFirst[benchConfig](func(a int) ReaderIOResult[benchConfig, string] { return Right[benchConfig]("logged") })
|
||||
chainer := ChainFirst(func(a int) ReaderIOResult[benchConfig, string] { return Right[benchConfig]("logged") })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
@@ -234,7 +234,7 @@ func BenchmarkChainFirst_Right(b *testing.B) {
|
||||
|
||||
func BenchmarkChainFirst_Left(b *testing.B) {
|
||||
rioe := Left[benchConfig, int](benchErr)
|
||||
chainer := ChainFirst[benchConfig](func(a int) ReaderIOResult[benchConfig, string] { return Right[benchConfig]("logged") })
|
||||
chainer := ChainFirst(func(a int) ReaderIOResult[benchConfig, string] { return Right[benchConfig]("logged") })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
@@ -443,7 +443,7 @@ func BenchmarkPipeline_Chain_Right(b *testing.B) {
|
||||
for b.Loop() {
|
||||
benchRIOE = F.Pipe1(
|
||||
rioe,
|
||||
Chain[benchConfig](func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x * 2) }),
|
||||
Chain(func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x * 2) }),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -455,7 +455,7 @@ func BenchmarkPipeline_Chain_Left(b *testing.B) {
|
||||
for b.Loop() {
|
||||
benchRIOE = F.Pipe1(
|
||||
rioe,
|
||||
Chain[benchConfig](func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x * 2) }),
|
||||
Chain(func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x * 2) }),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -468,7 +468,7 @@ func BenchmarkPipeline_Complex_Right(b *testing.B) {
|
||||
benchRIOE = F.Pipe3(
|
||||
rioe,
|
||||
Map[benchConfig](N.Mul(2)),
|
||||
Chain[benchConfig](func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x + 1) }),
|
||||
Chain(func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x + 1) }),
|
||||
Map[benchConfig](N.Mul(2)),
|
||||
)
|
||||
}
|
||||
@@ -482,7 +482,7 @@ func BenchmarkPipeline_Complex_Left(b *testing.B) {
|
||||
benchRIOE = F.Pipe3(
|
||||
rioe,
|
||||
Map[benchConfig](N.Mul(2)),
|
||||
Chain[benchConfig](func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x + 1) }),
|
||||
Chain(func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x + 1) }),
|
||||
Map[benchConfig](N.Mul(2)),
|
||||
)
|
||||
}
|
||||
@@ -492,7 +492,7 @@ func BenchmarkExecutePipeline_Complex_Right(b *testing.B) {
|
||||
rioe := F.Pipe3(
|
||||
Right[benchConfig](10),
|
||||
Map[benchConfig](N.Mul(2)),
|
||||
Chain[benchConfig](func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x + 1) }),
|
||||
Chain(func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x + 1) }),
|
||||
Map[benchConfig](N.Mul(2)),
|
||||
)
|
||||
b.ResetTimer()
|
||||
|
||||
@@ -17,11 +17,12 @@ package bracket
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/chain"
|
||||
)
|
||||
|
||||
// Bracket makes sure that a resource is cleaned up in the event of an error. The release action is called regardless of
|
||||
// whether the body action returns and error or not.
|
||||
func Bracket[
|
||||
func MonadBracket[
|
||||
GA, // IOEither[E, A]
|
||||
GB, // IOEither[E, A]
|
||||
GANY, // IOEither[E, ANY]
|
||||
@@ -50,3 +51,41 @@ func Bracket[
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Bracket makes sure that a resource is cleaned up in the event of an error. The release action is called regardless of
|
||||
// whether the body action returns and error or not.
|
||||
func Bracket[
|
||||
GA, // IOEither[E, A]
|
||||
GB, // IOEither[E, A]
|
||||
GANY, // IOEither[E, ANY]
|
||||
|
||||
EB, // Either[E, B]
|
||||
|
||||
A, B, ANY any](
|
||||
|
||||
ofeb func(EB) GB,
|
||||
|
||||
chainab chain.ChainType[A, GA, GB],
|
||||
chainebb chain.ChainType[EB, GB, GB],
|
||||
chainany chain.ChainType[ANY, GANY, GB],
|
||||
|
||||
acquire GA,
|
||||
use func(A) GB,
|
||||
release func(A, EB) GANY,
|
||||
) GB {
|
||||
return F.Pipe1(
|
||||
acquire,
|
||||
chainab(
|
||||
func(a A) GB {
|
||||
return F.Pipe1(
|
||||
use(a),
|
||||
chainebb(func(eb EB) GB {
|
||||
return F.Pipe1(
|
||||
release(a, eb),
|
||||
chainany(F.Constant1[ANY](ofeb(eb))),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -82,4 +82,6 @@ type (
|
||||
// Operator represents a transformation from one monadic value to another.
|
||||
// It takes a value in context HKTA and produces a value in context HKTB.
|
||||
Operator[HKTA, HKTB any] = func(HKTA) HKTB
|
||||
|
||||
ChainType[A, HKTA, HKTB any] = func(func(A) HKTB) func(HKTA) HKTB
|
||||
)
|
||||
|
||||
70
v2/internal/readert/monoid.go
Normal file
70
v2/internal/readert/monoid.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package readert
|
||||
|
||||
import (
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
)
|
||||
|
||||
// ApplySemigroup lifts a Semigroup[A] into a Semigroup[Reader[R, A]].
|
||||
// This allows you to combine two Readers that produce semigroup values by combining
|
||||
// their results using the semigroup's concat operation.
|
||||
//
|
||||
// The _map and _ap parameters are the Map and Ap operations for the Reader type,
|
||||
// typically obtained from the reader package.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Multiplier int }
|
||||
// // Using the additive semigroup for integers
|
||||
// intSemigroup := semigroup.MakeSemigroup(func(a, b int) int { return a + b })
|
||||
// readerSemigroup := reader.ApplySemigroup(
|
||||
// reader.MonadMap[Config, int, func(int) int],
|
||||
// reader.MonadAp[int, Config, int],
|
||||
// intSemigroup,
|
||||
// )
|
||||
//
|
||||
// r1 := reader.Of[Config](5)
|
||||
// r2 := reader.Of[Config](3)
|
||||
// combined := readerSemigroup.Concat(r1, r2)
|
||||
// result := combined(Config{Multiplier: 1}) // 8
|
||||
func ApplySemigroup[R, A any](
|
||||
_map func(func(R) A, func(A) func(A) A) func(R, func(A) A),
|
||||
_ap func(func(R, func(A) A), func(R) A) func(R) A,
|
||||
|
||||
s S.Semigroup[A],
|
||||
) S.Semigroup[func(R) A] {
|
||||
return S.ApplySemigroup(_map, _ap, s)
|
||||
}
|
||||
|
||||
// ApplicativeMonoid lifts a Monoid[A] into a Monoid[Reader[R, A]].
|
||||
// This allows you to combine Readers that produce monoid values, with an empty/identity Reader.
|
||||
//
|
||||
// The _of parameter is the Of operation (pure/return) for the Reader type.
|
||||
// The _map and _ap parameters are the Map and Ap operations for the Reader type.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Prefix string }
|
||||
// // Using the string concatenation monoid
|
||||
// stringMonoid := monoid.MakeMonoid("", func(a, b string) string { return a + b })
|
||||
// readerMonoid := reader.ApplicativeMonoid(
|
||||
// reader.Of[Config, string],
|
||||
// reader.MonadMap[Config, string, func(string) string],
|
||||
// reader.MonadAp[string, Config, string],
|
||||
// stringMonoid,
|
||||
// )
|
||||
//
|
||||
// r1 := reader.Asks(func(c Config) string { return c.Prefix })
|
||||
// r2 := reader.Of[Config]("hello")
|
||||
// combined := readerMonoid.Concat(r1, r2)
|
||||
// result := combined(Config{Prefix: ">> "}) // ">> hello"
|
||||
// empty := readerMonoid.Empty()(Config{Prefix: "any"}) // ""
|
||||
func ApplicativeMonoid[R, A any](
|
||||
_of func(A) func(R) A,
|
||||
_map func(func(R) A, func(A) func(A) A) func(R, func(A) A),
|
||||
_ap func(func(R, func(A) A), func(R) A) func(R) A,
|
||||
|
||||
m M.Monoid[A],
|
||||
) M.Monoid[func(R) A] {
|
||||
return M.ApplicativeMonoid(_of, _map, _ap, m)
|
||||
}
|
||||
@@ -17,6 +17,10 @@ package readert
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/apply"
|
||||
"github.com/IBM/fp-go/v2/internal/chain"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/pointed"
|
||||
R "github.com/IBM/fp-go/v2/reader/generic"
|
||||
)
|
||||
|
||||
@@ -32,7 +36,7 @@ func MonadMap[GEA ~func(E) HKTA, GEB ~func(E) HKTB, E, A, B, HKTA, HKTB any](
|
||||
}
|
||||
|
||||
func Map[GEA ~func(E) HKTA, GEB ~func(E) HKTB, E, A, B, HKTA, HKTB any](
|
||||
fmap func(func(A) B) func(HKTA) HKTB,
|
||||
fmap functor.MapType[A, B, HKTA, HKTB],
|
||||
f func(A) B,
|
||||
) func(GEA) GEB {
|
||||
return F.Pipe2(
|
||||
@@ -51,7 +55,7 @@ func MonadChain[GEA ~func(E) HKTA, GEB ~func(E) HKTB, A, E, HKTA, HKTB any](fcha
|
||||
}
|
||||
|
||||
func Chain[GEA ~func(E) HKTA, GEB ~func(E) HKTB, A, E, HKTA, HKTB any](
|
||||
fchain func(func(A) HKTB) func(HKTA) HKTB,
|
||||
fchain chain.ChainType[A, HKTA, HKTB],
|
||||
f func(A) GEB,
|
||||
) func(GEA) GEB {
|
||||
return func(ma GEA) GEB {
|
||||
@@ -63,7 +67,7 @@ func Chain[GEA ~func(E) HKTA, GEB ~func(E) HKTB, A, E, HKTA, HKTB any](
|
||||
}
|
||||
}
|
||||
|
||||
func MonadOf[GEA ~func(E) HKTA, E, A, HKTA any](fof func(A) HKTA, a A) GEA {
|
||||
func MonadOf[GEA ~func(E) HKTA, E, A, HKTA any](fof pointed.OfType[A, HKTA], a A) GEA {
|
||||
return R.MakeReader(func(_ E) HKTA {
|
||||
return fof(a)
|
||||
})
|
||||
@@ -76,7 +80,9 @@ func MonadAp[GEA ~func(E) HKTA, GEB ~func(E) HKTB, GEFAB ~func(E) HKTFAB, E, A,
|
||||
})
|
||||
}
|
||||
|
||||
func Ap[GEA ~func(E) HKTA, GEB ~func(E) HKTB, GEFAB ~func(E) HKTFAB, E, A, HKTA, HKTB, HKTFAB any](fap func(HKTA) func(HKTFAB) HKTB, fa GEA) func(GEFAB) GEB {
|
||||
func Ap[GEA ~func(E) HKTA, GEB ~func(E) HKTB, GEFAB ~func(E) HKTFAB, E, A, HKTA, HKTB, HKTFAB any](
|
||||
fap apply.ApType[HKTA, HKTB, HKTFAB],
|
||||
fa GEA) func(GEFAB) GEB {
|
||||
return func(fab GEFAB) GEB {
|
||||
return func(r E) HKTB {
|
||||
return fap(fa(r))(fab(r))
|
||||
@@ -85,11 +91,11 @@ func Ap[GEA ~func(E) HKTA, GEB ~func(E) HKTB, GEFAB ~func(E) HKTFAB, E, A, HKTA,
|
||||
}
|
||||
|
||||
func MonadFromReader[GA ~func(E) A, GEA ~func(E) HKTA, E, A, HKTA any](
|
||||
fof func(A) HKTA, ma GA) GEA {
|
||||
fof pointed.OfType[A, HKTA], ma GA) GEA {
|
||||
return R.MakeReader(F.Flow2(ma, fof))
|
||||
}
|
||||
|
||||
func FromReader[GA ~func(E) A, GEA ~func(E) HKTA, E, A, HKTA any](
|
||||
fof func(A) HKTA) func(ma GA) GEA {
|
||||
fof pointed.OfType[A, HKTA]) func(ma GA) GEA {
|
||||
return F.Bind1st(MonadFromReader[GA, GEA, E, A, HKTA], fof)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func Bracket[A, B, ANY any](
|
||||
use Kleisli[A, B],
|
||||
release func(A, B) IO[ANY],
|
||||
) IO[B] {
|
||||
return INTB.Bracket[IO[A], IO[B], IO[ANY], B, A, B](
|
||||
return INTB.MonadBracket[IO[A], IO[B], IO[ANY], B, A, B](
|
||||
Of[B],
|
||||
MonadChain[A, B],
|
||||
MonadChain[B, B],
|
||||
|
||||
@@ -35,7 +35,7 @@ import (
|
||||
//
|
||||
// safeOperation := io.WithLock(lock)(dangerousOperation)
|
||||
// result := safeOperation()
|
||||
func WithLock[A any](lock IO[context.CancelFunc]) func(fa IO[A]) IO[A] {
|
||||
func WithLock[A any](lock IO[context.CancelFunc]) Operator[A, A] {
|
||||
return func(fa IO[A]) IO[A] {
|
||||
return func() A {
|
||||
defer lock()()
|
||||
|
||||
@@ -27,7 +27,7 @@ func Bracket[E, A, B, ANY any](
|
||||
use Kleisli[E, A, B],
|
||||
release func(A, Either[E, B]) IOEither[E, ANY],
|
||||
) IOEither[E, B] {
|
||||
return BR.Bracket[IOEither[E, A], IOEither[E, B], IOEither[E, ANY], Either[E, B], A, B](
|
||||
return BR.MonadBracket[IOEither[E, A], IOEither[E, B], IOEither[E, ANY], Either[E, B], A, B](
|
||||
io.Of[Either[E, B]],
|
||||
MonadChain[E, A, B],
|
||||
io.MonadChain[Either[E, B], Either[E, B]],
|
||||
|
||||
@@ -27,7 +27,7 @@ func Bracket[A, B, ANY any](
|
||||
use Kleisli[A, B],
|
||||
release func(A, Option[B]) IOOption[ANY],
|
||||
) IOOption[B] {
|
||||
return G.Bracket[IOOption[A], IOOption[B], IOOption[ANY], Option[B], A, B](
|
||||
return G.MonadBracket[IOOption[A], IOOption[B], IOOption[ANY], Option[B], A, B](
|
||||
io.Of[Option[B]],
|
||||
MonadChain[A, B],
|
||||
io.MonadChain[Option[B], Option[B]],
|
||||
|
||||
54
v2/iterator/iter/last.go
Normal file
54
v2/iterator/iter/last.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package iter
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// Last returns the last element from an [Iterator] wrapped in an [Option].
|
||||
//
|
||||
// This function retrieves the last element from the iterator by consuming the entire
|
||||
// sequence. If the iterator contains at least one element, it returns Some(element).
|
||||
// If the iterator is empty, it returns None.
|
||||
//
|
||||
// RxJS Equivalent: [last] - https://rxjs.dev/api/operators/last
|
||||
//
|
||||
// Type Parameters:
|
||||
// - U: The type of elements in the iterator
|
||||
//
|
||||
// Parameters:
|
||||
// - it: The input iterator to get the last element from
|
||||
//
|
||||
// Returns:
|
||||
// - Option[U]: Some(last element) if the iterator is non-empty, None otherwise
|
||||
//
|
||||
// Example with non-empty sequence:
|
||||
//
|
||||
// seq := iter.From(1, 2, 3, 4, 5)
|
||||
// last := iter.Last(seq)
|
||||
// // Returns: Some(5)
|
||||
//
|
||||
// Example with empty sequence:
|
||||
//
|
||||
// seq := iter.Empty[int]()
|
||||
// last := iter.Last(seq)
|
||||
// // Returns: None
|
||||
//
|
||||
// Example with filtered sequence:
|
||||
//
|
||||
// seq := iter.From(1, 2, 3, 4, 5)
|
||||
// filtered := iter.Filter(func(x int) bool { return x < 4 })(seq)
|
||||
// last := iter.Last(filtered)
|
||||
// // Returns: Some(3)
|
||||
func Last[U any](it Seq[U]) Option[U] {
|
||||
var last U
|
||||
found := false
|
||||
|
||||
for last = range it {
|
||||
found = true
|
||||
}
|
||||
|
||||
if !found {
|
||||
return option.None[U]()
|
||||
}
|
||||
return option.Some(last)
|
||||
}
|
||||
305
v2/iterator/iter/last_test.go
Normal file
305
v2/iterator/iter/last_test.go
Normal file
@@ -0,0 +1,305 @@
|
||||
package iter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestLast test getting the last element from a non-empty sequence
|
||||
func TestLastSimple(t *testing.T) {
|
||||
|
||||
t.Run("returns last element from integer sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.Of(3), last)
|
||||
})
|
||||
|
||||
t.Run("returns last element from string sequence", func(t *testing.T) {
|
||||
seq := From("a", "b", "c")
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.Of("c"), last)
|
||||
})
|
||||
|
||||
t.Run("returns last element from single element sequence", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.Of(42), last)
|
||||
})
|
||||
|
||||
t.Run("returns last element from large sequence", func(t *testing.T) {
|
||||
seq := From(100, 200, 300, 400, 500)
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.Of(500), last)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLastEmpty tests getting the last element from an empty sequence
|
||||
func TestLastEmpty(t *testing.T) {
|
||||
|
||||
t.Run("returns None for empty integer sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.None[int](), last)
|
||||
})
|
||||
|
||||
t.Run("returns None for empty string sequence", func(t *testing.T) {
|
||||
seq := Empty[string]()
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.None[string](), last)
|
||||
})
|
||||
|
||||
t.Run("returns None for empty struct sequence", func(t *testing.T) {
|
||||
type TestStruct struct {
|
||||
Value int
|
||||
}
|
||||
seq := Empty[TestStruct]()
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.None[TestStruct](), last)
|
||||
})
|
||||
|
||||
t.Run("returns None for empty sequence of functions", func(t *testing.T) {
|
||||
type TestFunc func(int)
|
||||
seq := Empty[TestFunc]()
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.None[TestFunc](), last)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLastWithComplex tests Last with complex types
|
||||
func TestLastWithComplex(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("returns last person", func(t *testing.T) {
|
||||
seq := From(
|
||||
Person{"Alice", 30},
|
||||
Person{"Bob", 25},
|
||||
Person{"Charlie", 35},
|
||||
)
|
||||
last := Last(seq)
|
||||
expected := O.Of(Person{"Charlie", 35})
|
||||
assert.Equal(t, expected, last)
|
||||
})
|
||||
|
||||
t.Run("returns last pointer", func(t *testing.T) {
|
||||
p1 := &Person{"Alice", 30}
|
||||
p2 := &Person{"Bob", 25}
|
||||
seq := From(p1, p2)
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.Of(p2), last)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLastWithFunctions(t *testing.T) {
|
||||
|
||||
t.Run("return function", func(t *testing.T) {
|
||||
|
||||
want := "last"
|
||||
f1 := function.Constant("first")
|
||||
f2 := function.Constant("last")
|
||||
seq := From(f1, f2)
|
||||
|
||||
getLast := function.Flow2(
|
||||
Last,
|
||||
O.Map(funcReader),
|
||||
)
|
||||
assert.Equal(t, O.Of(want), getLast(seq))
|
||||
})
|
||||
}
|
||||
|
||||
func funcReader(f func() string) string {
|
||||
return f()
|
||||
}
|
||||
|
||||
// TestLastWithChan tests Last with channels
|
||||
func TestLastWithChan(t *testing.T) {
|
||||
t.Run("return function", func(t *testing.T) {
|
||||
want := 30
|
||||
seq := From(intChan(10),
|
||||
intChan(20),
|
||||
intChan(want))
|
||||
|
||||
getLast := function.Flow2(
|
||||
Last,
|
||||
O.Map(chanReader[int]),
|
||||
)
|
||||
assert.Equal(t, O.Of(want), getLast(seq))
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func chanReader[T any](c <-chan T) T {
|
||||
return <-c
|
||||
}
|
||||
|
||||
func intChan(val int) <-chan int {
|
||||
ch := make(chan int, 1)
|
||||
ch <- val
|
||||
close(ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
// TestLastWithChainedOperations tests Last with multiple chained operations
|
||||
func TestLastWithChainedOperations(t *testing.T) {
|
||||
t.Run("chains filter, map, and last", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
filtered := MonadFilter(seq, N.MoreThan(5))
|
||||
mapped := MonadMap(filtered, N.Mul(10))
|
||||
result := Last(mapped)
|
||||
assert.Equal(t, O.Of(100), result)
|
||||
})
|
||||
|
||||
t.Run("chains map and filter", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
mapped := MonadMap(seq, N.Mul(2))
|
||||
filtered := MonadFilter(mapped, N.MoreThan(5))
|
||||
result := Last(filtered)
|
||||
assert.Equal(t, O.Of(10), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLastWithReplicate tests Last with replicated values
|
||||
func TestLastWithReplicate(t *testing.T) {
|
||||
t.Run("returns last from replicated sequence", func(t *testing.T) {
|
||||
seq := Replicate(5, 42)
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.Of(42), last)
|
||||
})
|
||||
|
||||
t.Run("returns None from zero replications", func(t *testing.T) {
|
||||
seq := Replicate(0, 42)
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.None[int](), last)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLastWithMakeBy tests Last with MakeBy
|
||||
func TestLastWithMakeBy(t *testing.T) {
|
||||
t.Run("returns last generated element", func(t *testing.T) {
|
||||
seq := MakeBy(5, func(i int) int { return i * i })
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.Of(16), last)
|
||||
})
|
||||
|
||||
t.Run("returns None for zero elements", func(t *testing.T) {
|
||||
seq := MakeBy(0, F.Identity[int])
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.None[int](), last)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLastWithPrepend tests Last with Prepend
|
||||
func TestLastWithPrepend(t *testing.T) {
|
||||
t.Run("returns last element, not prepended", func(t *testing.T) {
|
||||
seq := From(2, 3, 4)
|
||||
prepended := Prepend(1)(seq)
|
||||
last := Last(prepended)
|
||||
assert.Equal(t, O.Of(4), last)
|
||||
})
|
||||
|
||||
t.Run("returns prepended element from empty sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
prepended := Prepend(42)(seq)
|
||||
last := Last(prepended)
|
||||
assert.Equal(t, O.Of(42), last)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLastWithAppend tests Last with Append
|
||||
func TestLastWithAppend(t *testing.T) {
|
||||
t.Run("returns appended element", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
appended := Append(4)(seq)
|
||||
last := Last(appended)
|
||||
assert.Equal(t, O.Of(4), last)
|
||||
})
|
||||
|
||||
t.Run("returns appended element from empty sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
appended := Append(42)(seq)
|
||||
last := Last(appended)
|
||||
assert.Equal(t, O.Of(42), last)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLastWithChain tests Last with Chain (flatMap)
|
||||
func TestLastWithChain(t *testing.T) {
|
||||
t.Run("returns last from chained sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
chained := MonadChain(seq, func(x int) Seq[int] {
|
||||
return From(x, x*10)
|
||||
})
|
||||
last := Last(chained)
|
||||
assert.Equal(t, O.Of(30), last)
|
||||
})
|
||||
|
||||
t.Run("returns None when chain produces empty", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
chained := MonadChain(seq, func(x int) Seq[int] {
|
||||
return Empty[int]()
|
||||
})
|
||||
last := Last(chained)
|
||||
assert.Equal(t, O.None[int](), last)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLastWithFlatten tests Last with Flatten
|
||||
func TestLastWithFlatten(t *testing.T) {
|
||||
t.Run("returns last from flattened sequence", func(t *testing.T) {
|
||||
nested := From(From(1, 2), From(3, 4), From(5))
|
||||
flattened := Flatten(nested)
|
||||
last := Last(flattened)
|
||||
assert.Equal(t, O.Of(5), last)
|
||||
})
|
||||
|
||||
t.Run("returns None from empty nested sequence", func(t *testing.T) {
|
||||
nested := Empty[Seq[int]]()
|
||||
flattened := Flatten(nested)
|
||||
last := Last(flattened)
|
||||
assert.Equal(t, O.None[int](), last)
|
||||
})
|
||||
}
|
||||
|
||||
// Example tests for documentation
|
||||
func ExampleLast() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
last := Last(seq)
|
||||
|
||||
if value, ok := O.Unwrap(last); ok {
|
||||
fmt.Printf("Last element: %d\n", value)
|
||||
}
|
||||
// Output: Last element: 5
|
||||
}
|
||||
|
||||
func ExampleLast_empty() {
|
||||
seq := Empty[int]()
|
||||
last := Last(seq)
|
||||
|
||||
if _, ok := O.Unwrap(last); !ok {
|
||||
fmt.Println("Sequence is empty")
|
||||
}
|
||||
// Output: Sequence is empty
|
||||
}
|
||||
|
||||
func ExampleLast_functions() {
|
||||
f1 := function.Constant("first")
|
||||
f2 := function.Constant("middle")
|
||||
f3 := function.Constant("last")
|
||||
seq := From(f1, f2, f3)
|
||||
|
||||
last := Last(seq)
|
||||
|
||||
if fn, ok := O.Unwrap(last); ok {
|
||||
result := fn()
|
||||
fmt.Printf("Last function result: %s\n", result)
|
||||
}
|
||||
// Output: Last function result: last
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user