mirror of
https://github.com/IBM/fp-go.git
synced 2026-01-21 01:07:29 +02:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfa48985ec | ||
|
|
677523b70f | ||
|
|
8243242cf1 | ||
|
|
9021a8e274 | ||
|
|
f3128e887b | ||
|
|
4583694211 | ||
|
|
b87c20d139 | ||
|
|
9fd5b90138 | ||
|
|
cdc2041d8e | ||
|
|
777fff9a5a |
1
v2/.bobignore
Normal file
1
v2/.bobignore
Normal file
@@ -0,0 +1 @@
|
||||
reflect\reflect.go
|
||||
@@ -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
|
||||
|
||||
@@ -514,6 +514,83 @@ func Push[A any](a A) Operator[A, A] {
|
||||
return G.Push[Operator[A, A]](a)
|
||||
}
|
||||
|
||||
// Concat concatenates two arrays, appending the provided array to the end of the input array.
|
||||
// This is a curried function that takes an array to append and returns a function that
|
||||
// takes the base array and returns the concatenated result.
|
||||
//
|
||||
// The function creates a new array containing all elements from the base array followed
|
||||
// by all elements from the appended array. Neither input array is modified.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of elements in the arrays
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The array to append to the end of the base array
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a base array and returns a new array with `as` appended to its end
|
||||
//
|
||||
// Behavior:
|
||||
// - Creates a new array with length equal to the sum of both input arrays
|
||||
// - Copies all elements from the base array first
|
||||
// - Appends all elements from the `as` array at the end
|
||||
// - Returns the base array unchanged if `as` is empty
|
||||
// - Returns `as` unchanged if the base array is empty
|
||||
// - Does not modify either input array
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// base := []int{1, 2, 3}
|
||||
// toAppend := []int{4, 5, 6}
|
||||
// result := array.Concat(toAppend)(base)
|
||||
// // result: []int{1, 2, 3, 4, 5, 6}
|
||||
// // base: []int{1, 2, 3} (unchanged)
|
||||
// // toAppend: []int{4, 5, 6} (unchanged)
|
||||
//
|
||||
// Example with empty arrays:
|
||||
//
|
||||
// base := []int{1, 2, 3}
|
||||
// empty := []int{}
|
||||
// result := array.Concat(empty)(base)
|
||||
// // result: []int{1, 2, 3}
|
||||
//
|
||||
// Example with strings:
|
||||
//
|
||||
// words1 := []string{"hello", "world"}
|
||||
// words2 := []string{"foo", "bar"}
|
||||
// result := array.Concat(words2)(words1)
|
||||
// // result: []string{"hello", "world", "foo", "bar"}
|
||||
//
|
||||
// Example with functional composition:
|
||||
//
|
||||
// numbers := []int{1, 2, 3}
|
||||
// result := F.Pipe2(
|
||||
// numbers,
|
||||
// array.Map(N.Mul(2)),
|
||||
// array.Concat([]int{10, 20}),
|
||||
// )
|
||||
// // result: []int{2, 4, 6, 10, 20}
|
||||
//
|
||||
// Use cases:
|
||||
// - Combining multiple arrays into one
|
||||
// - Building arrays incrementally
|
||||
// - Implementing array-based data structures (queues, buffers)
|
||||
// - Merging results from multiple operations
|
||||
// - Creating array pipelines with functional composition
|
||||
//
|
||||
// Performance:
|
||||
// - Time complexity: O(n + m) where n and m are the lengths of the arrays
|
||||
// - Space complexity: O(n + m) for the new array
|
||||
// - Optimized to avoid allocation when one array is empty
|
||||
//
|
||||
// Note: This function is immutable - it creates a new array rather than modifying
|
||||
// the input arrays. For appending a single element, consider using Append or Push.
|
||||
//
|
||||
//go:inline
|
||||
func Concat[A any](as []A) Operator[A, A] {
|
||||
return F.Bind2nd(array.Concat[[]A, A], as)
|
||||
}
|
||||
|
||||
// MonadFlap applies a value to an array of functions, producing an array of results.
|
||||
// This is the monadic version that takes both parameters.
|
||||
//
|
||||
|
||||
@@ -764,3 +764,341 @@ func TestExtendUseCases(t *testing.T) {
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestConcat tests the Concat function
|
||||
func TestConcat(t *testing.T) {
|
||||
t.Run("Concat two non-empty arrays", func(t *testing.T) {
|
||||
base := []int{1, 2, 3}
|
||||
toAppend := []int{4, 5, 6}
|
||||
result := Concat(toAppend)(base)
|
||||
expected := []int{1, 2, 3, 4, 5, 6}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Concat with empty array to append", func(t *testing.T) {
|
||||
base := []int{1, 2, 3}
|
||||
empty := []int{}
|
||||
result := Concat(empty)(base)
|
||||
assert.Equal(t, base, result)
|
||||
})
|
||||
|
||||
t.Run("Concat to empty base array", func(t *testing.T) {
|
||||
empty := []int{}
|
||||
toAppend := []int{1, 2, 3}
|
||||
result := Concat(toAppend)(empty)
|
||||
assert.Equal(t, toAppend, result)
|
||||
})
|
||||
|
||||
t.Run("Concat two empty arrays", func(t *testing.T) {
|
||||
empty1 := []int{}
|
||||
empty2 := []int{}
|
||||
result := Concat(empty2)(empty1)
|
||||
assert.Equal(t, []int{}, result)
|
||||
})
|
||||
|
||||
t.Run("Concat strings", func(t *testing.T) {
|
||||
words1 := []string{"hello", "world"}
|
||||
words2 := []string{"foo", "bar"}
|
||||
result := Concat(words2)(words1)
|
||||
expected := []string{"hello", "world", "foo", "bar"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Concat single element arrays", func(t *testing.T) {
|
||||
arr1 := []int{1}
|
||||
arr2 := []int{2}
|
||||
result := Concat(arr2)(arr1)
|
||||
expected := []int{1, 2}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Does not modify original arrays", func(t *testing.T) {
|
||||
base := []int{1, 2, 3}
|
||||
toAppend := []int{4, 5, 6}
|
||||
baseCopy := []int{1, 2, 3}
|
||||
toAppendCopy := []int{4, 5, 6}
|
||||
|
||||
_ = Concat(toAppend)(base)
|
||||
|
||||
assert.Equal(t, baseCopy, base)
|
||||
assert.Equal(t, toAppendCopy, toAppend)
|
||||
})
|
||||
|
||||
t.Run("Concat with floats", func(t *testing.T) {
|
||||
arr1 := []float64{1.1, 2.2}
|
||||
arr2 := []float64{3.3, 4.4}
|
||||
result := Concat(arr2)(arr1)
|
||||
expected := []float64{1.1, 2.2, 3.3, 4.4}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Concat with structs", func(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
arr1 := []Person{{"Alice", 30}, {"Bob", 25}}
|
||||
arr2 := []Person{{"Charlie", 35}}
|
||||
result := Concat(arr2)(arr1)
|
||||
expected := []Person{{"Alice", 30}, {"Bob", 25}, {"Charlie", 35}}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Concat large arrays", func(t *testing.T) {
|
||||
arr1 := MakeBy(500, F.Identity[int])
|
||||
arr2 := MakeBy(500, func(i int) int { return i + 500 })
|
||||
result := Concat(arr2)(arr1)
|
||||
|
||||
assert.Equal(t, 1000, len(result))
|
||||
assert.Equal(t, 0, result[0])
|
||||
assert.Equal(t, 499, result[499])
|
||||
assert.Equal(t, 500, result[500])
|
||||
assert.Equal(t, 999, result[999])
|
||||
})
|
||||
|
||||
t.Run("Concat multiple times", func(t *testing.T) {
|
||||
arr1 := []int{1}
|
||||
arr2 := []int{2}
|
||||
arr3 := []int{3}
|
||||
|
||||
result := F.Pipe2(
|
||||
arr1,
|
||||
Concat(arr2),
|
||||
Concat(arr3),
|
||||
)
|
||||
|
||||
expected := []int{1, 2, 3}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestConcatComposition tests Concat with other array operations
|
||||
func TestConcatComposition(t *testing.T) {
|
||||
t.Run("Concat after Map", func(t *testing.T) {
|
||||
numbers := []int{1, 2, 3}
|
||||
result := F.Pipe2(
|
||||
numbers,
|
||||
Map(N.Mul(2)),
|
||||
Concat([]int{10, 20}),
|
||||
)
|
||||
expected := []int{2, 4, 6, 10, 20}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Map after Concat", func(t *testing.T) {
|
||||
arr1 := []int{1, 2}
|
||||
arr2 := []int{3, 4}
|
||||
result := F.Pipe2(
|
||||
arr1,
|
||||
Concat(arr2),
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
expected := []int{2, 4, 6, 8}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Concat with Filter", func(t *testing.T) {
|
||||
arr1 := []int{1, 2, 3, 4}
|
||||
arr2 := []int{5, 6, 7, 8}
|
||||
result := F.Pipe2(
|
||||
arr1,
|
||||
Concat(arr2),
|
||||
Filter(func(n int) bool { return n%2 == 0 }),
|
||||
)
|
||||
expected := []int{2, 4, 6, 8}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Concat with Reduce", func(t *testing.T) {
|
||||
arr1 := []int{1, 2, 3}
|
||||
arr2 := []int{4, 5, 6}
|
||||
result := F.Pipe2(
|
||||
arr1,
|
||||
Concat(arr2),
|
||||
Reduce(func(acc, x int) int { return acc + x }, 0),
|
||||
)
|
||||
expected := 21 // 1+2+3+4+5+6
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Concat with Reverse", func(t *testing.T) {
|
||||
arr1 := []int{1, 2, 3}
|
||||
arr2 := []int{4, 5, 6}
|
||||
result := F.Pipe2(
|
||||
arr1,
|
||||
Concat(arr2),
|
||||
Reverse[int],
|
||||
)
|
||||
expected := []int{6, 5, 4, 3, 2, 1}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Concat with Flatten", func(t *testing.T) {
|
||||
arr1 := [][]int{{1, 2}, {3, 4}}
|
||||
arr2 := [][]int{{5, 6}}
|
||||
result := F.Pipe2(
|
||||
arr1,
|
||||
Concat(arr2),
|
||||
Flatten[int],
|
||||
)
|
||||
expected := []int{1, 2, 3, 4, 5, 6}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Multiple Concat operations", func(t *testing.T) {
|
||||
arr1 := []int{1}
|
||||
arr2 := []int{2}
|
||||
arr3 := []int{3}
|
||||
arr4 := []int{4}
|
||||
|
||||
result := Concat(arr4)(Concat(arr3)(Concat(arr2)(arr1)))
|
||||
|
||||
expected := []int{1, 2, 3, 4}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestConcatUseCases demonstrates practical use cases for Concat
|
||||
func TestConcatUseCases(t *testing.T) {
|
||||
t.Run("Building array incrementally", func(t *testing.T) {
|
||||
header := []string{"Name", "Age"}
|
||||
data := []string{"Alice", "30"}
|
||||
footer := []string{"Total: 1"}
|
||||
|
||||
result := F.Pipe2(
|
||||
header,
|
||||
Concat(data),
|
||||
Concat(footer),
|
||||
)
|
||||
|
||||
expected := []string{"Name", "Age", "Alice", "30", "Total: 1"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Merging results from multiple operations", func(t *testing.T) {
|
||||
evens := Filter(func(n int) bool { return n%2 == 0 })([]int{1, 2, 3, 4, 5, 6})
|
||||
odds := Filter(func(n int) bool { return n%2 != 0 })([]int{1, 2, 3, 4, 5, 6})
|
||||
|
||||
result := Concat(odds)(evens)
|
||||
expected := []int{2, 4, 6, 1, 3, 5}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Combining prefix and suffix", func(t *testing.T) {
|
||||
prefix := []string{"Mr.", "Dr."}
|
||||
names := []string{"Smith", "Jones"}
|
||||
|
||||
addPrefix := func(name string) []string {
|
||||
return Map(func(p string) string { return p + " " + name })(prefix)
|
||||
}
|
||||
|
||||
result := F.Pipe2(
|
||||
names,
|
||||
Chain(addPrefix),
|
||||
F.Identity[[]string],
|
||||
)
|
||||
|
||||
expected := []string{"Mr. Smith", "Dr. Smith", "Mr. Jones", "Dr. Jones"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Queue-like behavior", func(t *testing.T) {
|
||||
queue := []int{1, 2, 3}
|
||||
newItems := []int{4, 5}
|
||||
|
||||
// Add items to end of queue
|
||||
updatedQueue := Concat(newItems)(queue)
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, updatedQueue)
|
||||
assert.Equal(t, 1, updatedQueue[0]) // Front of queue
|
||||
assert.Equal(t, 5, updatedQueue[len(updatedQueue)-1]) // Back of queue
|
||||
})
|
||||
|
||||
t.Run("Combining configuration arrays", func(t *testing.T) {
|
||||
defaultConfig := []string{"--verbose", "--color"}
|
||||
userConfig := []string{"--output=file.txt", "--format=json"}
|
||||
|
||||
finalConfig := Concat(userConfig)(defaultConfig)
|
||||
|
||||
expected := []string{"--verbose", "--color", "--output=file.txt", "--format=json"}
|
||||
assert.Equal(t, expected, finalConfig)
|
||||
})
|
||||
}
|
||||
|
||||
// TestConcatProperties tests mathematical properties of Concat
|
||||
func TestConcatProperties(t *testing.T) {
|
||||
t.Run("Associativity: (a + b) + c == a + (b + c)", func(t *testing.T) {
|
||||
a := []int{1, 2}
|
||||
b := []int{3, 4}
|
||||
c := []int{5, 6}
|
||||
|
||||
// (a + b) + c
|
||||
left := Concat(c)(Concat(b)(a))
|
||||
|
||||
// a + (b + c)
|
||||
right := Concat(Concat(c)(b))(a)
|
||||
|
||||
assert.Equal(t, left, right)
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5, 6}, left)
|
||||
})
|
||||
|
||||
t.Run("Identity: a + [] == a and [] + a == a", func(t *testing.T) {
|
||||
arr := []int{1, 2, 3}
|
||||
empty := []int{}
|
||||
|
||||
// Right identity
|
||||
rightResult := Concat(empty)(arr)
|
||||
assert.Equal(t, arr, rightResult)
|
||||
|
||||
// Left identity
|
||||
leftResult := Concat(arr)(empty)
|
||||
assert.Equal(t, arr, leftResult)
|
||||
})
|
||||
|
||||
t.Run("Length property: len(a + b) == len(a) + len(b)", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
arr1 []int
|
||||
arr2 []int
|
||||
}{
|
||||
{[]int{1, 2, 3}, []int{4, 5}},
|
||||
{[]int{1}, []int{2, 3, 4, 5}},
|
||||
{[]int{}, []int{1, 2, 3}},
|
||||
{[]int{1, 2, 3}, []int{}},
|
||||
{MakeBy(100, F.Identity[int]), MakeBy(50, F.Identity[int])},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
result := Concat(tc.arr2)(tc.arr1)
|
||||
expectedLen := len(tc.arr1) + len(tc.arr2)
|
||||
assert.Equal(t, expectedLen, len(result))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Order preservation: elements maintain their relative order", func(t *testing.T) {
|
||||
arr1 := []int{1, 2, 3}
|
||||
arr2 := []int{4, 5, 6}
|
||||
result := Concat(arr2)(arr1)
|
||||
|
||||
// Check arr1 elements are in order
|
||||
assert.Equal(t, 1, result[0])
|
||||
assert.Equal(t, 2, result[1])
|
||||
assert.Equal(t, 3, result[2])
|
||||
|
||||
// Check arr2 elements are in order after arr1
|
||||
assert.Equal(t, 4, result[3])
|
||||
assert.Equal(t, 5, result[4])
|
||||
assert.Equal(t, 6, result[5])
|
||||
})
|
||||
|
||||
t.Run("Immutability: original arrays are not modified", func(t *testing.T) {
|
||||
original1 := []int{1, 2, 3}
|
||||
original2 := []int{4, 5, 6}
|
||||
copy1 := []int{1, 2, 3}
|
||||
copy2 := []int{4, 5, 6}
|
||||
|
||||
_ = Concat(original2)(original1)
|
||||
|
||||
assert.Equal(t, copy1, original1)
|
||||
assert.Equal(t, copy2, original2)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,7 +16,11 @@
|
||||
package array
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/internal/apply"
|
||||
"github.com/IBM/fp-go/v2/internal/array"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/pointed"
|
||||
"github.com/IBM/fp-go/v2/internal/traversable"
|
||||
)
|
||||
|
||||
// Traverse maps each element of an array to an effect (HKT), then collects the results
|
||||
@@ -55,9 +59,9 @@ import (
|
||||
//
|
||||
//go:inline
|
||||
func Traverse[A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func([]B) HKTRB,
|
||||
fmap func(func([]B) func(B) []B) func(HKTRB) HKTAB,
|
||||
fap func(HKTB) func(HKTAB) HKTRB,
|
||||
fof pointed.OfType[[]B, HKTRB],
|
||||
fmap functor.MapType[[]B, func(B) []B, HKTRB, HKTAB],
|
||||
fap apply.ApType[HKTB, HKTRB, HKTAB],
|
||||
|
||||
f func(A) HKTB) func([]A) HKTRB {
|
||||
return array.Traverse[[]A](fof, fmap, fap, f)
|
||||
@@ -71,7 +75,7 @@ func Traverse[A, B, HKTB, HKTAB, HKTRB any](
|
||||
//
|
||||
//go:inline
|
||||
func MonadTraverse[A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func([]B) HKTRB,
|
||||
fof pointed.OfType[[]B, HKTRB],
|
||||
fmap func(func([]B) func(B) []B) func(HKTRB) HKTAB,
|
||||
fap func(HKTB) func(HKTAB) HKTRB,
|
||||
|
||||
@@ -83,7 +87,7 @@ func MonadTraverse[A, B, HKTB, HKTAB, HKTRB any](
|
||||
|
||||
//go:inline
|
||||
func TraverseWithIndex[A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func([]B) HKTRB,
|
||||
fof pointed.OfType[[]B, HKTRB],
|
||||
fmap func(func([]B) func(B) []B) func(HKTRB) HKTAB,
|
||||
fap func(HKTB) func(HKTAB) HKTRB,
|
||||
|
||||
@@ -93,7 +97,7 @@ func TraverseWithIndex[A, B, HKTB, HKTAB, HKTRB any](
|
||||
|
||||
//go:inline
|
||||
func MonadTraverseWithIndex[A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func([]B) HKTRB,
|
||||
fof pointed.OfType[[]B, HKTRB],
|
||||
fmap func(func([]B) func(B) []B) func(HKTRB) HKTAB,
|
||||
fap func(HKTB) func(HKTAB) HKTRB,
|
||||
|
||||
@@ -102,3 +106,22 @@ func MonadTraverseWithIndex[A, B, HKTB, HKTAB, HKTRB any](
|
||||
|
||||
return array.MonadTraverseWithIndex(fof, fmap, fap, ta, f)
|
||||
}
|
||||
|
||||
func MakeTraverseType[A, B, HKT_F_B, HKT_F_T_B, HKT_F_B_T_B any]() traversable.TraverseType[A, B, []A, []B, HKT_F_B, HKT_F_T_B, HKT_F_B_T_B] {
|
||||
return func(
|
||||
// ap
|
||||
fof_b pointed.OfType[[]B, HKT_F_T_B],
|
||||
fmap_b functor.MapType[[]B, func(B) []B, HKT_F_T_B, HKT_F_B_T_B],
|
||||
fap_b apply.ApType[HKT_F_B, HKT_F_T_B, HKT_F_B_T_B],
|
||||
|
||||
) func(func(A) HKT_F_B) func([]A) HKT_F_T_B {
|
||||
return func(f func(A) HKT_F_B) func([]A) HKT_F_T_B {
|
||||
return Traverse(
|
||||
fof_b,
|
||||
fmap_b,
|
||||
fap_b,
|
||||
f,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,81 @@
|
||||
// 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 builder provides a generic Builder pattern interface for constructing
|
||||
// complex objects with validation.
|
||||
//
|
||||
// The Builder pattern is useful when:
|
||||
// - Object construction requires multiple steps
|
||||
// - Construction may fail with validation errors
|
||||
// - You want to separate construction logic from the object itself
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// type PersonBuilder struct {
|
||||
// name string
|
||||
// age int
|
||||
// }
|
||||
//
|
||||
// func (b PersonBuilder) Build() result.Result[Person] {
|
||||
// if b.name == "" {
|
||||
// return result.Error[Person](errors.New("name is required"))
|
||||
// }
|
||||
// if b.age < 0 {
|
||||
// return result.Error[Person](errors.New("age must be non-negative"))
|
||||
// }
|
||||
// return result.Of(Person{Name: b.name, Age: b.age})
|
||||
// }
|
||||
package builder
|
||||
|
||||
type (
|
||||
// Builder is a generic interface for the Builder pattern that constructs
|
||||
// objects of type T with validation.
|
||||
//
|
||||
// The Build method returns a Result[T] which can be either:
|
||||
// - Success: containing the constructed object of type T
|
||||
// - Error: containing an error if validation or construction fails
|
||||
//
|
||||
// This allows builders to perform validation and return meaningful errors
|
||||
// during the construction process, making it explicit that object creation
|
||||
// may fail.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of object being built
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type ConfigBuilder struct {
|
||||
// host string
|
||||
// port int
|
||||
// }
|
||||
//
|
||||
// func (b ConfigBuilder) Build() result.Result[Config] {
|
||||
// if b.host == "" {
|
||||
// return result.Error[Config](errors.New("host is required"))
|
||||
// }
|
||||
// if b.port <= 0 || b.port > 65535 {
|
||||
// return result.Error[Config](errors.New("invalid port"))
|
||||
// }
|
||||
// return result.Of(Config{Host: b.host, Port: b.port})
|
||||
// }
|
||||
Builder[T any] interface {
|
||||
// Build constructs and validates an object of type T.
|
||||
//
|
||||
// Returns:
|
||||
// - Result[T]: A Result containing either the successfully built object
|
||||
// or an error if validation or construction fails.
|
||||
Build() Result[T]
|
||||
}
|
||||
)
|
||||
|
||||
374
v2/builder/builder_test.go
Normal file
374
v2/builder/builder_test.go
Normal file
@@ -0,0 +1,374 @@
|
||||
// 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 builder
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Test types for demonstration
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
type PersonBuilder struct {
|
||||
name string
|
||||
age int
|
||||
}
|
||||
|
||||
func (b PersonBuilder) WithName(name string) PersonBuilder {
|
||||
b.name = name
|
||||
return b
|
||||
}
|
||||
|
||||
func (b PersonBuilder) WithAge(age int) PersonBuilder {
|
||||
b.age = age
|
||||
return b
|
||||
}
|
||||
|
||||
func (b PersonBuilder) Build() Result[Person] {
|
||||
if b.name == "" {
|
||||
return result.Left[Person](errors.New("name is required"))
|
||||
}
|
||||
if b.age < 0 {
|
||||
return result.Left[Person](errors.New("age must be non-negative"))
|
||||
}
|
||||
if b.age > 150 {
|
||||
return result.Left[Person](errors.New("age must be realistic"))
|
||||
}
|
||||
return result.Of(Person{Name: b.name, Age: b.age})
|
||||
}
|
||||
|
||||
func NewPersonBuilder(p Person) PersonBuilder {
|
||||
return PersonBuilder{name: p.Name, age: p.Age}
|
||||
}
|
||||
|
||||
// Config example for additional test coverage
|
||||
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
type ConfigBuilder struct {
|
||||
host string
|
||||
port int
|
||||
}
|
||||
|
||||
func (b ConfigBuilder) WithHost(host string) ConfigBuilder {
|
||||
b.host = host
|
||||
return b
|
||||
}
|
||||
|
||||
func (b ConfigBuilder) WithPort(port int) ConfigBuilder {
|
||||
b.port = port
|
||||
return b
|
||||
}
|
||||
|
||||
func (b ConfigBuilder) Build() Result[Config] {
|
||||
if b.host == "" {
|
||||
return result.Left[Config](errors.New("host is required"))
|
||||
}
|
||||
if b.port <= 0 || b.port > 65535 {
|
||||
return result.Left[Config](errors.New("port must be between 1 and 65535"))
|
||||
}
|
||||
return result.Of(Config{Host: b.host, Port: b.port})
|
||||
}
|
||||
|
||||
func NewConfigBuilder(c Config) ConfigBuilder {
|
||||
return ConfigBuilder{host: c.Host, port: c.Port}
|
||||
}
|
||||
|
||||
// Tests for Builder interface
|
||||
|
||||
func TestBuilder_SuccessfulBuild(t *testing.T) {
|
||||
builder := PersonBuilder{}.
|
||||
WithName("Alice").
|
||||
WithAge(30)
|
||||
|
||||
res := builder.Build()
|
||||
|
||||
assert.True(t, result.IsRight(res), "Build should succeed")
|
||||
person := result.ToOption(res)
|
||||
assert.True(t, O.IsSome(person), "Result should contain a person")
|
||||
|
||||
p := O.GetOrElse(func() Person { return Person{} })(person)
|
||||
assert.Equal(t, "Alice", p.Name)
|
||||
assert.Equal(t, 30, p.Age)
|
||||
}
|
||||
|
||||
func TestBuilder_ValidationFailure_MissingName(t *testing.T) {
|
||||
builder := PersonBuilder{}.WithAge(30)
|
||||
|
||||
res := builder.Build()
|
||||
|
||||
assert.True(t, result.IsLeft(res), "Build should fail when name is missing")
|
||||
err := result.Fold(
|
||||
func(e error) error { return e },
|
||||
func(Person) error { return errors.New("unexpected success") },
|
||||
)(res)
|
||||
assert.Equal(t, "name is required", err.Error())
|
||||
}
|
||||
|
||||
func TestBuilder_ValidationFailure_NegativeAge(t *testing.T) {
|
||||
builder := PersonBuilder{}.
|
||||
WithName("Bob").
|
||||
WithAge(-5)
|
||||
|
||||
res := builder.Build()
|
||||
|
||||
assert.True(t, result.IsLeft(res), "Build should fail when age is negative")
|
||||
err := result.Fold(
|
||||
func(e error) error { return e },
|
||||
func(Person) error { return errors.New("unexpected success") },
|
||||
)(res)
|
||||
assert.Equal(t, "age must be non-negative", err.Error())
|
||||
}
|
||||
|
||||
func TestBuilder_ValidationFailure_UnrealisticAge(t *testing.T) {
|
||||
builder := PersonBuilder{}.
|
||||
WithName("Charlie").
|
||||
WithAge(200)
|
||||
|
||||
res := builder.Build()
|
||||
|
||||
assert.True(t, result.IsLeft(res), "Build should fail when age is unrealistic")
|
||||
err := result.Fold(
|
||||
func(e error) error { return e },
|
||||
func(Person) error { return errors.New("unexpected success") },
|
||||
)(res)
|
||||
assert.Equal(t, "age must be realistic", err.Error())
|
||||
}
|
||||
|
||||
func TestBuilder_ConfigSuccessfulBuild(t *testing.T) {
|
||||
builder := ConfigBuilder{}.
|
||||
WithHost("localhost").
|
||||
WithPort(8080)
|
||||
|
||||
res := builder.Build()
|
||||
|
||||
assert.True(t, result.IsRight(res), "Build should succeed")
|
||||
config := result.ToOption(res)
|
||||
assert.True(t, O.IsSome(config), "Result should contain a config")
|
||||
|
||||
c := O.GetOrElse(func() Config { return Config{} })(config)
|
||||
assert.Equal(t, "localhost", c.Host)
|
||||
assert.Equal(t, 8080, c.Port)
|
||||
}
|
||||
|
||||
func TestBuilder_ConfigValidationFailure_MissingHost(t *testing.T) {
|
||||
builder := ConfigBuilder{}.WithPort(8080)
|
||||
|
||||
res := builder.Build()
|
||||
|
||||
assert.True(t, result.IsLeft(res), "Build should fail when host is missing")
|
||||
err := result.Fold(
|
||||
func(e error) error { return e },
|
||||
func(Config) error { return errors.New("unexpected success") },
|
||||
)(res)
|
||||
assert.Equal(t, "host is required", err.Error())
|
||||
}
|
||||
|
||||
func TestBuilder_ConfigValidationFailure_InvalidPort(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
port int
|
||||
}{
|
||||
{"zero port", 0},
|
||||
{"negative port", -1},
|
||||
{"port too large", 70000},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
builder := ConfigBuilder{}.
|
||||
WithHost("localhost").
|
||||
WithPort(tt.port)
|
||||
|
||||
res := builder.Build()
|
||||
|
||||
assert.True(t, result.IsLeft(res), "Build should fail for invalid port")
|
||||
err := result.Fold(
|
||||
func(e error) error { return e },
|
||||
func(Config) error { return errors.New("unexpected success") },
|
||||
)(res)
|
||||
assert.Equal(t, "port must be between 1 and 65535", err.Error())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Tests for BuilderPrism
|
||||
|
||||
func TestBuilderPrism_GetOption_ValidBuilder(t *testing.T) {
|
||||
prism := BuilderPrism(NewPersonBuilder)
|
||||
|
||||
builder := PersonBuilder{}.
|
||||
WithName("Alice").
|
||||
WithAge(30)
|
||||
|
||||
personOpt := prism.GetOption(builder)
|
||||
|
||||
assert.True(t, O.IsSome(personOpt), "GetOption should return Some for valid builder")
|
||||
person := O.GetOrElse(func() Person { return Person{} })(personOpt)
|
||||
assert.Equal(t, "Alice", person.Name)
|
||||
assert.Equal(t, 30, person.Age)
|
||||
}
|
||||
|
||||
func TestBuilderPrism_GetOption_InvalidBuilder(t *testing.T) {
|
||||
prism := BuilderPrism(NewPersonBuilder)
|
||||
|
||||
builder := PersonBuilder{}.WithAge(30) // Missing name
|
||||
|
||||
personOpt := prism.GetOption(builder)
|
||||
|
||||
assert.True(t, O.IsNone(personOpt), "GetOption should return None for invalid builder")
|
||||
}
|
||||
|
||||
func TestBuilderPrism_ReverseGet(t *testing.T) {
|
||||
prism := BuilderPrism(NewPersonBuilder)
|
||||
|
||||
person := Person{Name: "Bob", Age: 25}
|
||||
|
||||
builder := prism.ReverseGet(person)
|
||||
|
||||
assert.Equal(t, "Bob", builder.name)
|
||||
assert.Equal(t, 25, builder.age)
|
||||
|
||||
// Verify the builder can build the same person
|
||||
res := builder.Build()
|
||||
assert.True(t, result.IsRight(res), "Builder from ReverseGet should be valid")
|
||||
|
||||
rebuilt := O.GetOrElse(func() Person { return Person{} })(result.ToOption(res))
|
||||
assert.Equal(t, person, rebuilt)
|
||||
}
|
||||
|
||||
func TestBuilderPrism_RoundTrip_ValidBuilder(t *testing.T) {
|
||||
prism := BuilderPrism(NewPersonBuilder)
|
||||
|
||||
originalBuilder := PersonBuilder{}.
|
||||
WithName("Charlie").
|
||||
WithAge(35)
|
||||
|
||||
// Extract person from builder
|
||||
personOpt := prism.GetOption(originalBuilder)
|
||||
assert.True(t, O.IsSome(personOpt), "Should extract person from valid builder")
|
||||
|
||||
person := O.GetOrElse(func() Person { return Person{} })(personOpt)
|
||||
|
||||
// Reconstruct builder from person
|
||||
rebuiltBuilder := prism.ReverseGet(person)
|
||||
|
||||
// Verify the rebuilt builder produces the same person
|
||||
rebuiltRes := rebuiltBuilder.Build()
|
||||
assert.True(t, result.IsRight(rebuiltRes), "Rebuilt builder should be valid")
|
||||
|
||||
rebuiltPerson := O.GetOrElse(func() Person { return Person{} })(result.ToOption(rebuiltRes))
|
||||
assert.Equal(t, person, rebuiltPerson)
|
||||
}
|
||||
|
||||
func TestBuilderPrism_ConfigPrism(t *testing.T) {
|
||||
prism := BuilderPrism(NewConfigBuilder)
|
||||
|
||||
builder := ConfigBuilder{}.
|
||||
WithHost("example.com").
|
||||
WithPort(443)
|
||||
|
||||
configOpt := prism.GetOption(builder)
|
||||
|
||||
assert.True(t, O.IsSome(configOpt), "GetOption should return Some for valid config builder")
|
||||
config := O.GetOrElse(func() Config { return Config{} })(configOpt)
|
||||
assert.Equal(t, "example.com", config.Host)
|
||||
assert.Equal(t, 443, config.Port)
|
||||
}
|
||||
|
||||
func TestBuilderPrism_ConfigPrism_InvalidBuilder(t *testing.T) {
|
||||
prism := BuilderPrism(NewConfigBuilder)
|
||||
|
||||
builder := ConfigBuilder{}.WithPort(8080) // Missing host
|
||||
|
||||
configOpt := prism.GetOption(builder)
|
||||
|
||||
assert.True(t, O.IsNone(configOpt), "GetOption should return None for invalid config builder")
|
||||
}
|
||||
|
||||
func TestBuilderPrism_ConfigPrism_ReverseGet(t *testing.T) {
|
||||
prism := BuilderPrism(NewConfigBuilder)
|
||||
|
||||
config := Config{Host: "api.example.com", Port: 9000}
|
||||
|
||||
builder := prism.ReverseGet(config)
|
||||
|
||||
assert.Equal(t, "api.example.com", builder.host)
|
||||
assert.Equal(t, 9000, builder.port)
|
||||
|
||||
// Verify the builder can build the same config
|
||||
res := builder.Build()
|
||||
assert.True(t, result.IsRight(res), "Builder from ReverseGet should be valid")
|
||||
|
||||
rebuilt := O.GetOrElse(func() Config { return Config{} })(result.ToOption(res))
|
||||
assert.Equal(t, config, rebuilt)
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
|
||||
func BenchmarkBuilder_SuccessfulBuild(b *testing.B) {
|
||||
builder := PersonBuilder{}.
|
||||
WithName("Alice").
|
||||
WithAge(30)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = builder.Build()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBuilder_FailedBuild(b *testing.B) {
|
||||
builder := PersonBuilder{}.WithAge(30) // Missing name
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = builder.Build()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBuilderPrism_GetOption(b *testing.B) {
|
||||
prism := BuilderPrism(NewPersonBuilder)
|
||||
builder := PersonBuilder{}.
|
||||
WithName("Alice").
|
||||
WithAge(30)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = prism.GetOption(builder)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBuilderPrism_ReverseGet(b *testing.B) {
|
||||
prism := BuilderPrism(NewPersonBuilder)
|
||||
person := Person{Name: "Bob", Age: 25}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = prism.ReverseGet(person)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,18 @@
|
||||
// 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 builder
|
||||
|
||||
import (
|
||||
@@ -6,7 +21,61 @@ import (
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// BuilderPrism createa a [Prism] that converts between a builder and its type
|
||||
// BuilderPrism creates a [Prism] that converts between a builder and its built type.
|
||||
//
|
||||
// A Prism is an optic that focuses on a case of a sum type, providing bidirectional
|
||||
// conversion with the possibility of failure. This function creates a prism that:
|
||||
// - Extracts: Attempts to build the object from the builder (may fail)
|
||||
// - Constructs: Creates a builder from a valid object (always succeeds)
|
||||
//
|
||||
// The extraction direction (builder -> object) uses the Build method and converts
|
||||
// the Result to an Option, where errors become None. The construction direction
|
||||
// (object -> builder) uses the provided creator function.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of the object being built
|
||||
// - B: The builder type that implements Builder[T]
|
||||
//
|
||||
// Parameters:
|
||||
// - creator: A function that creates a builder from a valid object of type T.
|
||||
// This function should initialize the builder with all fields from the object.
|
||||
//
|
||||
// Returns:
|
||||
// - Prism[B, T]: A prism that can convert between the builder and the built type.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// type PersonBuilder struct {
|
||||
// name string
|
||||
// age int
|
||||
// }
|
||||
//
|
||||
// func (b PersonBuilder) Build() result.Result[Person] {
|
||||
// if b.name == "" {
|
||||
// return result.Error[Person](errors.New("name required"))
|
||||
// }
|
||||
// return result.Of(Person{Name: b.name, Age: b.age})
|
||||
// }
|
||||
//
|
||||
// func NewPersonBuilder(p Person) PersonBuilder {
|
||||
// return PersonBuilder{name: p.Name, age: p.Age}
|
||||
// }
|
||||
//
|
||||
// // Create a prism for PersonBuilder
|
||||
// prism := BuilderPrism(NewPersonBuilder)
|
||||
//
|
||||
// // Use the prism to extract a Person from a valid builder
|
||||
// builder := PersonBuilder{name: "Alice", age: 30}
|
||||
// person := prism.GetOption(builder) // Some(Person{Name: "Alice", Age: 30})
|
||||
//
|
||||
// // Use the prism to create a builder from a Person
|
||||
// p := Person{Name: "Bob", Age: 25}
|
||||
// b := prism.ReverseGet(p) // PersonBuilder{name: "Bob", age: 25}
|
||||
func BuilderPrism[T any, B Builder[T]](creator func(T) B) Prism[B, T] {
|
||||
return prism.MakePrismWithName(F.Flow2(B.Build, result.ToOption[T]), creator, "BuilderPrism")
|
||||
}
|
||||
|
||||
@@ -560,6 +560,63 @@ func Read[A any](r context.Context) func(ReaderIO[A]) IO[A] {
|
||||
return RIO.Read[A](r)
|
||||
}
|
||||
|
||||
// ReadIO executes a ReaderIO computation by providing a context wrapped in an IO effect.
|
||||
// This is useful when the context itself needs to be computed or retrieved through side effects.
|
||||
//
|
||||
// The function takes an IO[context.Context] (an effectful computation that produces a context) and returns
|
||||
// a function that can execute a ReaderIO[A] to produce an IO[A].
|
||||
//
|
||||
// This is particularly useful in scenarios where:
|
||||
// - The context needs to be created with side effects (e.g., loading configuration)
|
||||
// - The context requires initialization or setup
|
||||
// - You want to compose context creation with the computation that uses it
|
||||
//
|
||||
// The execution flow is:
|
||||
// 1. Execute the IO[context.Context] to get the context
|
||||
// 2. Pass the context to the ReaderIO[A] to get an IO[A]
|
||||
// 3. Execute the resulting IO[A] to get the final result A
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The result type of the ReaderIO computation
|
||||
//
|
||||
// Parameters:
|
||||
// - r: An IO effect that produces a context.Context
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderIO[A] and returns an IO[A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// "context"
|
||||
// G "github.com/IBM/fp-go/v2/io"
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// )
|
||||
//
|
||||
// // Create context with side effects (e.g., loading config)
|
||||
// createContext := G.Of(context.WithValue(context.Background(), "key", "value"))
|
||||
//
|
||||
// // A computation that uses the context
|
||||
// getValue := readerio.FromReader(func(ctx context.Context) string {
|
||||
// if val := ctx.Value("key"); val != nil {
|
||||
// return val.(string)
|
||||
// }
|
||||
// return "default"
|
||||
// })
|
||||
//
|
||||
// // Compose them together
|
||||
// result := readerio.ReadIO[string](createContext)(getValue)
|
||||
// value := result() // Executes both effects and returns "value"
|
||||
//
|
||||
// Comparison with Read:
|
||||
// - [Read]: Takes a pure context.Context value and executes the ReaderIO immediately
|
||||
// - [ReadIO]: Takes an IO[context.Context] and chains the effects together
|
||||
//
|
||||
//go:inline
|
||||
func ReadIO[A any](r IO[context.Context]) func(ReaderIO[A]) IO[A] {
|
||||
return RIO.ReadIO[A](r)
|
||||
}
|
||||
|
||||
// Local transforms the context.Context environment before passing it to a ReaderIO computation.
|
||||
//
|
||||
// This is the Reader's local operation, which allows you to modify the environment
|
||||
|
||||
@@ -500,3 +500,188 @@ func TestTapWithLogging(t *testing.T) {
|
||||
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"))
|
||||
rio := FromReader(func(ctx context.Context) string {
|
||||
if val := ctx.Value("testKey"); val != nil {
|
||||
return val.(string)
|
||||
}
|
||||
return "default"
|
||||
})
|
||||
|
||||
ioAction := ReadIO[string](contextIO)(rio)
|
||||
result := ioAction()
|
||||
|
||||
assert.Equal(t, "testValue", result)
|
||||
}
|
||||
|
||||
func TestReadIOWithBackground(t *testing.T) {
|
||||
// Test ReadIO with plain background context
|
||||
contextIO := G.Of(context.Background())
|
||||
rio := Of(42)
|
||||
|
||||
ioAction := ReadIO[int](contextIO)(rio)
|
||||
result := ioAction()
|
||||
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
func TestReadIOWithChain(t *testing.T) {
|
||||
// Test ReadIO with chained operations
|
||||
contextIO := G.Of(context.WithValue(context.Background(), "multiplier", 3))
|
||||
|
||||
result := F.Pipe1(
|
||||
FromReader(func(ctx context.Context) int {
|
||||
if val := ctx.Value("multiplier"); val != nil {
|
||||
return val.(int)
|
||||
}
|
||||
return 1
|
||||
}),
|
||||
Chain(func(n int) ReaderIO[int] {
|
||||
return Of(n * 10)
|
||||
}),
|
||||
)
|
||||
|
||||
ioAction := ReadIO[int](contextIO)(result)
|
||||
value := ioAction()
|
||||
|
||||
assert.Equal(t, 30, value) // 3 * 10
|
||||
}
|
||||
|
||||
func TestReadIOWithMap(t *testing.T) {
|
||||
// Test ReadIO with Map operations
|
||||
contextIO := G.Of(context.Background())
|
||||
|
||||
result := F.Pipe2(
|
||||
Of(5),
|
||||
Map(N.Mul(2)),
|
||||
Map(N.Add(10)),
|
||||
)
|
||||
|
||||
ioAction := ReadIO[int](contextIO)(result)
|
||||
value := ioAction()
|
||||
|
||||
assert.Equal(t, 20, value) // (5 * 2) + 10
|
||||
}
|
||||
|
||||
func TestReadIOWithSideEffects(t *testing.T) {
|
||||
// Test ReadIO with side effects in context creation
|
||||
counter := 0
|
||||
contextIO := func() context.Context {
|
||||
counter++
|
||||
return context.WithValue(context.Background(), "counter", counter)
|
||||
}
|
||||
|
||||
rio := FromReader(func(ctx context.Context) int {
|
||||
if val := ctx.Value("counter"); val != nil {
|
||||
return val.(int)
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
ioAction := ReadIO[int](contextIO)(rio)
|
||||
result := ioAction()
|
||||
|
||||
assert.Equal(t, 1, result)
|
||||
assert.Equal(t, 1, counter)
|
||||
}
|
||||
|
||||
func TestReadIOMultipleExecutions(t *testing.T) {
|
||||
// Test that ReadIO creates fresh effects on each execution
|
||||
counter := 0
|
||||
contextIO := func() context.Context {
|
||||
counter++
|
||||
return context.Background()
|
||||
}
|
||||
|
||||
rio := Of(42)
|
||||
ioAction := ReadIO[int](contextIO)(rio)
|
||||
|
||||
result1 := ioAction()
|
||||
result2 := ioAction()
|
||||
|
||||
assert.Equal(t, 42, result1)
|
||||
assert.Equal(t, 42, result2)
|
||||
assert.Equal(t, 2, counter) // Context IO executed twice
|
||||
}
|
||||
|
||||
func TestReadIOComparisonWithRead(t *testing.T) {
|
||||
// Compare ReadIO with Read to show the difference
|
||||
ctx := context.WithValue(context.Background(), "key", "value")
|
||||
|
||||
rio := FromReader(func(ctx context.Context) string {
|
||||
if val := ctx.Value("key"); val != nil {
|
||||
return val.(string)
|
||||
}
|
||||
return "default"
|
||||
})
|
||||
|
||||
// Using Read (direct context)
|
||||
ioAction1 := Read[string](ctx)(rio)
|
||||
result1 := ioAction1()
|
||||
|
||||
// Using ReadIO (context wrapped in IO)
|
||||
contextIO := G.Of(ctx)
|
||||
ioAction2 := ReadIO[string](contextIO)(rio)
|
||||
result2 := ioAction2()
|
||||
|
||||
assert.Equal(t, result1, result2)
|
||||
assert.Equal(t, "value", result1)
|
||||
assert.Equal(t, "value", result2)
|
||||
}
|
||||
|
||||
func TestReadIOWithComplexContext(t *testing.T) {
|
||||
// Test ReadIO with complex context manipulation
|
||||
type contextKey string
|
||||
const (
|
||||
userKey contextKey = "user"
|
||||
tokenKey contextKey = "token"
|
||||
)
|
||||
|
||||
contextIO := G.Of(
|
||||
context.WithValue(
|
||||
context.WithValue(context.Background(), userKey, "Alice"),
|
||||
tokenKey,
|
||||
"secret123",
|
||||
),
|
||||
)
|
||||
|
||||
rio := FromReader(func(ctx context.Context) map[string]string {
|
||||
result := make(map[string]string)
|
||||
if user := ctx.Value(userKey); user != nil {
|
||||
result["user"] = user.(string)
|
||||
}
|
||||
if token := ctx.Value(tokenKey); token != nil {
|
||||
result["token"] = token.(string)
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
ioAction := ReadIO[map[string]string](contextIO)(rio)
|
||||
result := ioAction()
|
||||
|
||||
assert.Equal(t, "Alice", result["user"])
|
||||
assert.Equal(t, "secret123", result["token"])
|
||||
}
|
||||
|
||||
func TestReadIOWithAsk(t *testing.T) {
|
||||
// Test ReadIO combined with Ask
|
||||
contextIO := G.Of(context.WithValue(context.Background(), "data", 100))
|
||||
|
||||
result := F.Pipe1(
|
||||
Ask(),
|
||||
Map(func(ctx context.Context) int {
|
||||
if val := ctx.Value("data"); val != nil {
|
||||
return val.(int)
|
||||
}
|
||||
return 0
|
||||
}),
|
||||
)
|
||||
|
||||
ioAction := ReadIO[int](contextIO)(result)
|
||||
value := ioAction()
|
||||
|
||||
assert.Equal(t, 100, value)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
```
|
||||
|
||||
|
||||
@@ -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)()
|
||||
}
|
||||
|
||||
@@ -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())())
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
// Example_sequenceReader_basicUsage demonstrates the basic usage of SequenceReader
|
||||
@@ -233,7 +234,7 @@ func Example_sequenceReaderResult_errorHandling() {
|
||||
ctx := context.Background()
|
||||
pipeline := F.Pipe2(
|
||||
sequenced(ctx),
|
||||
RIOE.Map(func(x int) int { return x * 2 }),
|
||||
RIOE.Map(N.Mul(2)),
|
||||
RIOE.Chain(func(x int) RIOE.ReaderIOResult[string] {
|
||||
return RIOE.Of(fmt.Sprintf("Result: %d", x))
|
||||
}),
|
||||
|
||||
@@ -914,6 +914,21 @@ func Read[A any](r context.Context) func(ReaderIOResult[A]) IOResult[A] {
|
||||
return RIOR.Read[A](r)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ReadIO[A any](r IO[context.Context]) func(ReaderIOResult[A]) IOResult[A] {
|
||||
return RIOR.ReadIO[A](r)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ReadIOEither[A any](r IOResult[context.Context]) func(ReaderIOResult[A]) IOResult[A] {
|
||||
return RIOR.ReadIOEither[A](r)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ReadIOResult[A any](r IOResult[context.Context]) func(ReaderIOResult[A]) IOResult[A] {
|
||||
return RIOR.ReadIOResult[A](r)
|
||||
}
|
||||
|
||||
// MonadChainLeft chains a computation on the left (error) side of a [ReaderIOResult].
|
||||
// If the input is a Left value, it applies the function f to transform the error and potentially
|
||||
// change the error type. If the input is a Right value, it passes through unchanged.
|
||||
@@ -1026,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:
|
||||
//
|
||||
@@ -1097,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:
|
||||
//
|
||||
@@ -1106,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)
|
||||
@@ -1158,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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
96
v2/context/readerreaderioresult/bracket.go
Normal file
96
v2/context/readerreaderioresult/bracket.go
Normal file
@@ -0,0 +1,96 @@
|
||||
// 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 func(A) ReaderReaderIOResult[R, 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[AppConfig](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[AppConfig](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[Config1, Config2, int](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[Config1, Config2, int](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[Config1, Config2, string](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[Config1, Config2, int](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[Config1, Config2, int](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[Config1, Config2, int](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[Config1, Config2, int](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[Config1, Config2, string](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[Config1, Config2, int](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[Config1, Config2, int](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[Config1, Config2, int](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[Config1, Config2, int](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[Config1, Config2, string](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[Config1, Config2, int](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[Config1, Config2, int](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, Config1, int, string](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, Config1, int, string](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, Config1, int, string](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, Config1, int, string](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, Config1, int, int](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, Config1, int, int](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, Config1, int, string](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, Config1, int, string](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, Config1, int, int](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, Config1, int, int](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, Config1, int, int](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, Config1, int, int](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[Config1, Config2, int](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, Config1, int, string](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[Config1, Config2, int](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[Config1, Config2, int](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[Config1, Config2, int](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, Config1, int, string](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, Config1, int, string](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, error](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, error](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, error](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, error](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, error](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[R, context.Context](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, R](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[R, context.Context, error, error, A](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[R, context.Context, error, error, A](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[AppConfig](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[AppConfig](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[AppConfig](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, int](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[AppConfig](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[AppConfig](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, int](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[AppConfig](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[AppConfig](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[AppConfig](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[AppConfig, int](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[AppConfig](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, AppConfig, OtherConfig](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[AppConfig](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[AppConfig](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[AppConfig](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[AppConfig](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[AppConfig](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, AppConfig](fa),
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
}
|
||||
97
v2/context/readerreaderioresult/retry.go
Normal file
97
v2/context/readerreaderioresult/retry.go
Normal file
@@ -0,0 +1,97 @@
|
||||
// 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"
|
||||
|
||||
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 func(r R) ReaderIOResult[context.Context, A] {
|
||||
return RIOE.Retrying(policy, F.Pipe1(action, reader.Map[retry.RetryStatus](reader.Read[ReaderIOResult[context.Context, A]](r))), check)
|
||||
}
|
||||
}
|
||||
265
v2/context/readerreaderioresult/retry_test.go
Normal file
265
v2/context/readerreaderioresult/retry_test.go
Normal file
@@ -0,0 +1,265 @@
|
||||
// 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"
|
||||
|
||||
"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 := func(r Result[int]) bool {
|
||||
return result.IsLeft(r)
|
||||
}
|
||||
|
||||
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 := func(r Result[int]) bool {
|
||||
return result.IsLeft(r)
|
||||
}
|
||||
|
||||
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 := func(r Result[int]) bool {
|
||||
return result.IsLeft(r)
|
||||
}
|
||||
|
||||
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 := func(r Result[int]) bool {
|
||||
return result.IsLeft(r)
|
||||
}
|
||||
|
||||
// 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 := func(r Result[int]) bool {
|
||||
return result.IsLeft(r)
|
||||
}
|
||||
|
||||
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 := func(r Result[int]) bool {
|
||||
return result.IsLeft(r)
|
||||
}
|
||||
|
||||
// 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(
|
||||
func(error) bool { return true },
|
||||
func(v int) bool { return v < 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
|
||||
)
|
||||
246
v2/context/readerresult/IO_OPERATIONS_RATIONALE.md
Normal file
246
v2/context/readerresult/IO_OPERATIONS_RATIONALE.md
Normal file
@@ -0,0 +1,246 @@
|
||||
# Why Combining IO Operations with ReaderResult Makes Sense
|
||||
|
||||
## Overview
|
||||
|
||||
The `context/readerresult` package provides functions that combine IO operations (like `FromIO`, `ChainIOK`, `TapIOK`, etc.) with ReaderResult computations. This document explains why this combination is natural and appropriate, despite IO operations being side-effectful.
|
||||
|
||||
## Key Insight: ReaderResult is Already Effectful
|
||||
|
||||
**IMPORTANT**: Unlike pure functional Reader monads, `ReaderResult[A]` in this package is **already side-effectful** because it depends on `context.Context`.
|
||||
|
||||
### Why context.Context is Effectful
|
||||
|
||||
The `context.Context` type in Go is inherently effectful because it:
|
||||
|
||||
1. **Can be cancelled**: `ctx.Done()` returns a channel that closes when the context is cancelled
|
||||
2. **Has deadlines**: `ctx.Deadline()` returns a time when the context expires
|
||||
3. **Carries values**: `ctx.Value(key)` retrieves request-scoped values
|
||||
4. **Propagates signals**: Cancellation signals propagate across goroutines
|
||||
5. **Has observable state**: The context's state can change over time (e.g., when cancelled)
|
||||
|
||||
### Type Definition
|
||||
|
||||
```go
|
||||
type ReaderResult[A any] = func(context.Context) Result[A]
|
||||
```
|
||||
|
||||
This is **not** a pure function because:
|
||||
- The behavior can change based on the context's state
|
||||
- The context can be cancelled during execution
|
||||
- The context carries mutable, observable state
|
||||
|
||||
## Comparison with Pure Reader Monads
|
||||
|
||||
### Pure Reader (from `readerresult` package)
|
||||
|
||||
```go
|
||||
type ReaderResult[R, A any] = func(R) Result[A]
|
||||
```
|
||||
|
||||
- `R` can be any type (config, state, etc.)
|
||||
- The function is **pure** if `R` is immutable
|
||||
- No side effects unless explicitly introduced
|
||||
|
||||
### Effectful Reader (from `context/readerresult` package)
|
||||
|
||||
```go
|
||||
type ReaderResult[A any] = func(context.Context) Result[A]
|
||||
```
|
||||
|
||||
- Always depends on `context.Context`
|
||||
- **Inherently effectful** due to context's nature
|
||||
- Side effects are part of the design
|
||||
|
||||
## Why IO Operations Fit Naturally
|
||||
|
||||
Since `ReaderResult` is already effectful, combining it with IO operations is a natural fit:
|
||||
|
||||
### 1. Both Represent Side Effects
|
||||
|
||||
```go
|
||||
// IO operation - side effectful
|
||||
io := func() int {
|
||||
fmt.Println("Performing IO")
|
||||
return 42
|
||||
}
|
||||
|
||||
// ReaderResult - also side effectful (depends on context)
|
||||
rr := func(ctx context.Context) Result[int] {
|
||||
// Can check if context is cancelled (side effect)
|
||||
if ctx.Err() != nil {
|
||||
return result.Error[int](ctx.Err())
|
||||
}
|
||||
return result.Of(42)
|
||||
}
|
||||
|
||||
// Combining them is natural
|
||||
combined := FromIO(io)
|
||||
```
|
||||
|
||||
### 2. Context-Aware IO Operations
|
||||
|
||||
The combination allows IO operations to respect context cancellation:
|
||||
|
||||
```go
|
||||
// IO operation that should respect cancellation
|
||||
readFile := func(path string) ReaderResult[[]byte] {
|
||||
return func(ctx context.Context) Result[[]byte] {
|
||||
// Check cancellation before expensive IO
|
||||
if ctx.Err() != nil {
|
||||
return result.Error[[]byte](ctx.Err())
|
||||
}
|
||||
|
||||
// Perform IO operation
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return result.Error[[]byte](err)
|
||||
}
|
||||
return result.Of(data)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Practical Use Cases
|
||||
|
||||
#### Logging with Side Effects
|
||||
|
||||
```go
|
||||
// Log to external system (IO operation)
|
||||
logMetric := func(value int) func() string {
|
||||
return func() string {
|
||||
// Side effect: write to metrics system
|
||||
metrics.Record("value", value)
|
||||
return "logged"
|
||||
}
|
||||
}
|
||||
|
||||
// Use with ReaderResult
|
||||
pipeline := F.Pipe1(
|
||||
readerresult.Of(42),
|
||||
readerresult.TapIOK(logMetric),
|
||||
)
|
||||
```
|
||||
|
||||
#### Database Operations
|
||||
|
||||
```go
|
||||
// Database query (IO operation with context)
|
||||
queryDB := func(id int) ReaderResult[User] {
|
||||
return func(ctx context.Context) Result[User] {
|
||||
// Context used for timeout/cancellation
|
||||
user, err := db.QueryContext(ctx, "SELECT * FROM users WHERE id = ?", id)
|
||||
if err != nil {
|
||||
return result.Error[User](err)
|
||||
}
|
||||
return result.Of(user)
|
||||
}
|
||||
}
|
||||
|
||||
// Chain with other operations
|
||||
pipeline := F.Pipe2(
|
||||
readerresult.Of(123),
|
||||
readerresult.Chain(queryDB),
|
||||
readerresult.TapIOK(func(user User) func() string {
|
||||
return func() string {
|
||||
log.Printf("Retrieved user: %s", user.Name)
|
||||
return "logged"
|
||||
}
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
#### HTTP Requests
|
||||
|
||||
```go
|
||||
// HTTP request (IO operation)
|
||||
fetchData := func(url string) ReaderResult[Response] {
|
||||
return func(ctx context.Context) Result[Response] {
|
||||
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return result.Error[Response](err)
|
||||
}
|
||||
return result.Of(resp)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Functions That Combine IO with ReaderResult
|
||||
|
||||
### Lifting Functions
|
||||
|
||||
- **`FromIO[A]`**: Lifts a pure IO computation into ReaderResult
|
||||
- **`FromIOResult[A]`**: Lifts an IOResult (IO with error handling) into ReaderResult
|
||||
|
||||
### Chaining Functions
|
||||
|
||||
- **`ChainIOK[A, B]`**: Sequences a ReaderResult with an IO computation
|
||||
- **`ChainIOEitherK[A, B]`**: Sequences with an IOResult computation
|
||||
- **`ChainIOResultK[A, B]`**: Alias for ChainIOEitherK
|
||||
|
||||
### Tapping Functions (Side Effects)
|
||||
|
||||
- **`TapIOK[A, B]`**: Executes IO for side effects, preserves original value
|
||||
- **`ChainFirstIOK[A, B]`**: Same as TapIOK
|
||||
- **`MonadTapIOK[A, B]`**: Monadic version of TapIOK
|
||||
- **`MonadChainFirstIOK[A, B]`**: Monadic version of ChainFirstIOK
|
||||
|
||||
### Error Handling with IO
|
||||
|
||||
- **`TapLeftIOK[A, B]`**: Executes IO on error for side effects (logging, metrics)
|
||||
- **`ChainFirstLeftIOK[A, B]`**: Same as TapLeftIOK
|
||||
|
||||
### Reading Context from IO
|
||||
|
||||
- **`ReadIO[A]`**: Executes ReaderResult with context from IO
|
||||
- **`ReadIOEither[A]`**: Executes with context from IOResult
|
||||
- **`ReadIOResult[A]`**: Alias for ReadIOEither
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
### Embrace Effectfulness
|
||||
|
||||
Rather than trying to maintain purity (which is impossible with `context.Context`), this package embraces the effectful nature of Go's context and provides tools to work with it safely and composably.
|
||||
|
||||
### Composition Over Isolation
|
||||
|
||||
The package allows you to compose effectful operations (ReaderResult + IO) in a type-safe, functional way, rather than isolating them.
|
||||
|
||||
### Practical Go Idioms
|
||||
|
||||
This approach aligns with Go's pragmatic philosophy:
|
||||
- Context is used everywhere in Go for cancellation and timeouts
|
||||
- IO operations are common and necessary
|
||||
- Combining them in a type-safe way improves code quality
|
||||
|
||||
## Contrast with Pure Functional Packages
|
||||
|
||||
### When to Use `context/readerresult` (This Package)
|
||||
|
||||
Use when you need:
|
||||
- ✅ Context cancellation and timeouts
|
||||
- ✅ Request-scoped values
|
||||
- ✅ Integration with Go's standard library (http, database/sql, etc.)
|
||||
- ✅ IO operations with error handling
|
||||
- ✅ Practical, idiomatic Go code
|
||||
|
||||
### When to Use `readerresult` (Pure Package)
|
||||
|
||||
Use when you need:
|
||||
- ✅ Pure dependency injection
|
||||
- ✅ Testable computations with simple config objects
|
||||
- ✅ No context propagation
|
||||
- ✅ Generic environment types (not limited to context.Context)
|
||||
- ✅ Purely functional composition
|
||||
|
||||
## Conclusion
|
||||
|
||||
Combining IO operations with ReaderResult in the `context/readerresult` package makes sense because:
|
||||
|
||||
1. **ReaderResult is already effectful** due to its dependency on `context.Context`
|
||||
2. **IO operations are also effectful**, making them a natural fit
|
||||
3. **The combination provides practical benefits** for real-world Go applications
|
||||
4. **It aligns with Go's pragmatic philosophy** of embracing side effects when necessary
|
||||
5. **It enables type-safe composition** of effectful operations
|
||||
|
||||
The key insight is that `context.Context` itself is a side effect, so adding more side effects (IO operations) doesn't violate any purity constraints—because there were none to begin with. This package provides tools to work with these side effects in a safe, composable, and type-safe manner.
|
||||
@@ -16,7 +16,6 @@
|
||||
package readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
@@ -42,7 +41,7 @@ func TestBind(t *testing.T) {
|
||||
Map(utils.GetFullName),
|
||||
)
|
||||
|
||||
assert.Equal(t, res(context.Background()), E.Of[error]("John Doe"))
|
||||
assert.Equal(t, res(t.Context()), E.Of[error]("John Doe"))
|
||||
}
|
||||
|
||||
func TestApS(t *testing.T) {
|
||||
@@ -54,5 +53,5 @@ func TestApS(t *testing.T) {
|
||||
Map(utils.GetFullName),
|
||||
)
|
||||
|
||||
assert.Equal(t, res(context.Background()), E.Of[error]("John Doe"))
|
||||
assert.Equal(t, res(t.Context()), E.Of[error]("John Doe"))
|
||||
}
|
||||
|
||||
@@ -22,7 +22,75 @@ import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
// withContext wraps an existing ReaderResult and performs a context check for cancellation before deletating
|
||||
// WithContext wraps an existing ReaderResult and performs a context check for cancellation
|
||||
// before delegating to the wrapped computation. This provides early cancellation detection,
|
||||
// allowing computations to fail fast when the context has been cancelled or has exceeded
|
||||
// its deadline.
|
||||
//
|
||||
// IMPORTANT: This function checks for context cancellation BEFORE executing the wrapped
|
||||
// ReaderResult. If the context is already cancelled or has exceeded its deadline, the
|
||||
// computation returns immediately with the cancellation error without executing the
|
||||
// wrapped ReaderResult.
|
||||
//
|
||||
// The function uses context.Cause(ctx) to extract the cancellation reason, which may be:
|
||||
// - context.Canceled: The context was explicitly cancelled
|
||||
// - context.DeadlineExceeded: The context's deadline was exceeded
|
||||
// - A custom error: If the context was cancelled with a cause (Go 1.20+)
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type of the ReaderResult
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The ReaderResult to wrap with cancellation checking
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderResult that checks for cancellation before executing ma
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a long-running computation
|
||||
// longComputation := func(ctx context.Context) result.Result[int] {
|
||||
// time.Sleep(5 * time.Second)
|
||||
// return result.Of(42)
|
||||
// }
|
||||
//
|
||||
// // Wrap with cancellation check
|
||||
// safeLongComputation := readerresult.WithContext(longComputation)
|
||||
//
|
||||
// // Cancel the context before execution
|
||||
// ctx, cancel := context.WithCancel(t.Context())
|
||||
// cancel()
|
||||
//
|
||||
// // The computation returns immediately with cancellation error
|
||||
// result := safeLongComputation(ctx)
|
||||
// // result is Left(context.Canceled) - longComputation never executes
|
||||
//
|
||||
// Example with timeout:
|
||||
//
|
||||
// fetchData := func(ctx context.Context) result.Result[string] {
|
||||
// // Simulate slow operation
|
||||
// time.Sleep(2 * time.Second)
|
||||
// return result.Of("data")
|
||||
// }
|
||||
//
|
||||
// safeFetch := readerresult.WithContext(fetchData)
|
||||
//
|
||||
// // Context with 1 second timeout
|
||||
// ctx, cancel := context.WithTimeout(t.Context(), 1*time.Second)
|
||||
// defer cancel()
|
||||
//
|
||||
// time.Sleep(1500 * time.Millisecond) // Wait for timeout
|
||||
//
|
||||
// result := safeFetch(ctx)
|
||||
// // result is Left(context.DeadlineExceeded) - fetchData never executes
|
||||
//
|
||||
// Use cases:
|
||||
// - Wrapping expensive computations to enable early cancellation
|
||||
// - Preventing unnecessary work when context is already cancelled
|
||||
// - Implementing timeout-aware operations
|
||||
// - Building cancellation-aware pipelines
|
||||
//
|
||||
//go:inline
|
||||
func WithContext[A any](ma ReaderResult[A]) ReaderResult[A] {
|
||||
return func(ctx context.Context) E.Either[error, A] {
|
||||
if ctx.Err() != nil {
|
||||
@@ -32,6 +100,81 @@ func WithContext[A any](ma ReaderResult[A]) ReaderResult[A] {
|
||||
}
|
||||
}
|
||||
|
||||
// WithContextK wraps a Kleisli arrow with context cancellation checking.
|
||||
// This is a higher-order function that takes a Kleisli arrow and returns a new
|
||||
// Kleisli arrow that checks for context cancellation before executing.
|
||||
//
|
||||
// IMPORTANT: This function composes the Kleisli arrow with WithContext, ensuring
|
||||
// that the resulting ReaderResult checks for cancellation before execution. This
|
||||
// is particularly useful when building pipelines of Kleisli arrows where you want
|
||||
// cancellation checking at each step.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The input type of the Kleisli arrow
|
||||
// - B: The output type of the Kleisli arrow
|
||||
//
|
||||
// Parameters:
|
||||
// - f: The Kleisli arrow to wrap with cancellation checking
|
||||
//
|
||||
// Returns:
|
||||
// - A new Kleisli arrow that checks for cancellation before executing f
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Define a Kleisli arrow
|
||||
// processUser := func(id int) readerresult.ReaderResult[User] {
|
||||
// return func(ctx context.Context) result.Result[User] {
|
||||
// // Expensive database operation
|
||||
// return fetchUserFromDB(ctx, id)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Wrap with cancellation checking
|
||||
// safeProcessUser := readerresult.WithContextK(processUser)
|
||||
//
|
||||
// // Use in a pipeline
|
||||
// pipeline := F.Pipe1(
|
||||
// readerresult.Of(123),
|
||||
// readerresult.Chain(safeProcessUser),
|
||||
// )
|
||||
//
|
||||
// // If context is cancelled, processUser never executes
|
||||
// ctx, cancel := context.WithCancel(t.Context())
|
||||
// cancel()
|
||||
// result := pipeline(ctx) // Left(context.Canceled)
|
||||
//
|
||||
// Example with multiple steps:
|
||||
//
|
||||
// getUserK := readerresult.WithContextK(func(id int) readerresult.ReaderResult[User] {
|
||||
// return func(ctx context.Context) result.Result[User] {
|
||||
// return fetchUser(ctx, id)
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// getOrdersK := readerresult.WithContextK(func(user User) readerresult.ReaderResult[[]Order] {
|
||||
// return func(ctx context.Context) result.Result[[]Order] {
|
||||
// return fetchOrders(ctx, user.ID)
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// // Each step checks for cancellation
|
||||
// pipeline := F.Pipe2(
|
||||
// readerresult.Of(123),
|
||||
// readerresult.Chain(getUserK),
|
||||
// readerresult.Chain(getOrdersK),
|
||||
// )
|
||||
//
|
||||
// // If context is cancelled at any point, remaining steps don't execute
|
||||
// ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
|
||||
// defer cancel()
|
||||
// result := pipeline(ctx)
|
||||
//
|
||||
// Use cases:
|
||||
// - Building cancellation-aware pipelines
|
||||
// - Ensuring each step in a chain respects cancellation
|
||||
// - Implementing timeout-aware multi-step operations
|
||||
// - Preventing cascading failures in long pipelines
|
||||
//
|
||||
//go:inline
|
||||
func WithContextK[A, B any](f Kleisli[A, B]) Kleisli[A, B] {
|
||||
return F.Flow2(
|
||||
|
||||
418
v2/context/readerresult/cancel_test.go
Normal file
418
v2/context/readerresult/cancel_test.go
Normal file
@@ -0,0 +1,418 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestWithContext tests the WithContext function
|
||||
func TestWithContext(t *testing.T) {
|
||||
t.Run("executes wrapped ReaderResult when context is not cancelled", func(t *testing.T) {
|
||||
executed := false
|
||||
computation := func(ctx context.Context) E.Either[error, int] {
|
||||
executed = true
|
||||
return E.Of[error](42)
|
||||
}
|
||||
|
||||
wrapped := WithContext(computation)
|
||||
result := wrapped(t.Context())
|
||||
|
||||
assert.True(t, executed, "computation should be executed")
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
})
|
||||
|
||||
t.Run("returns cancellation error when context is cancelled", func(t *testing.T) {
|
||||
executed := false
|
||||
computation := func(ctx context.Context) E.Either[error, int] {
|
||||
executed = true
|
||||
return E.Of[error](42)
|
||||
}
|
||||
|
||||
wrapped := WithContext(computation)
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
result := wrapped(ctx)
|
||||
|
||||
assert.False(t, executed, "computation should not be executed when context is cancelled")
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, err := E.UnwrapError(result)
|
||||
assert.Equal(t, context.Canceled, err)
|
||||
})
|
||||
|
||||
t.Run("returns deadline exceeded error when context times out", func(t *testing.T) {
|
||||
executed := false
|
||||
computation := func(ctx context.Context) E.Either[error, int] {
|
||||
executed = true
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
return E.Of[error](42)
|
||||
}
|
||||
|
||||
wrapped := WithContext(computation)
|
||||
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
time.Sleep(20 * time.Millisecond) // Wait for timeout
|
||||
|
||||
result := wrapped(ctx)
|
||||
|
||||
assert.False(t, executed, "computation should not be executed when context has timed out")
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, err := E.UnwrapError(result)
|
||||
assert.Equal(t, context.DeadlineExceeded, err)
|
||||
})
|
||||
|
||||
t.Run("preserves errors from wrapped computation", func(t *testing.T) {
|
||||
testErr := errors.New("computation error")
|
||||
computation := func(ctx context.Context) E.Either[error, int] {
|
||||
return E.Left[int](testErr)
|
||||
}
|
||||
|
||||
wrapped := WithContext(computation)
|
||||
result := wrapped(t.Context())
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, err := E.UnwrapError(result)
|
||||
assert.Equal(t, testErr, err)
|
||||
})
|
||||
|
||||
t.Run("prevents expensive computation when context is already cancelled", func(t *testing.T) {
|
||||
expensiveExecuted := false
|
||||
expensiveComputation := func(ctx context.Context) E.Either[error, int] {
|
||||
expensiveExecuted = true
|
||||
// Simulate expensive operation
|
||||
time.Sleep(1 * time.Second)
|
||||
return E.Of[error](42)
|
||||
}
|
||||
|
||||
wrapped := WithContext(expensiveComputation)
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
start := time.Now()
|
||||
result := wrapped(ctx)
|
||||
duration := time.Since(start)
|
||||
|
||||
assert.False(t, expensiveExecuted, "expensive computation should not execute")
|
||||
assert.True(t, E.IsLeft(result))
|
||||
assert.Less(t, duration, 100*time.Millisecond, "should return immediately")
|
||||
})
|
||||
|
||||
t.Run("works with context.WithCancelCause", func(t *testing.T) {
|
||||
executed := false
|
||||
computation := func(ctx context.Context) E.Either[error, int] {
|
||||
executed = true
|
||||
return E.Of[error](42)
|
||||
}
|
||||
|
||||
wrapped := WithContext(computation)
|
||||
|
||||
customErr := errors.New("custom cancellation reason")
|
||||
ctx, cancel := context.WithCancelCause(t.Context())
|
||||
cancel(customErr)
|
||||
|
||||
result := wrapped(ctx)
|
||||
|
||||
assert.False(t, executed, "computation should not be executed")
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, err := E.UnwrapError(result)
|
||||
assert.Equal(t, customErr, err)
|
||||
})
|
||||
|
||||
t.Run("can be nested for multiple cancellation checks", func(t *testing.T) {
|
||||
executed := false
|
||||
computation := func(ctx context.Context) E.Either[error, int] {
|
||||
executed = true
|
||||
return E.Of[error](42)
|
||||
}
|
||||
|
||||
doubleWrapped := WithContext(WithContext(computation))
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
result := doubleWrapped(ctx)
|
||||
|
||||
assert.False(t, executed, "computation should not be executed")
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestWithContextK tests the WithContextK function
|
||||
func TestWithContextK(t *testing.T) {
|
||||
t.Run("wraps Kleisli arrow with cancellation checking", func(t *testing.T) {
|
||||
executed := false
|
||||
processUser := func(id int) ReaderResult[string] {
|
||||
return func(ctx context.Context) E.Either[error, string] {
|
||||
executed = true
|
||||
return E.Of[error]("user-" + string(rune(id+48)))
|
||||
}
|
||||
}
|
||||
|
||||
safeProcessUser := WithContextK(processUser)
|
||||
|
||||
result := safeProcessUser(123)(t.Context())
|
||||
|
||||
assert.True(t, executed, "Kleisli should be executed")
|
||||
assert.True(t, E.IsRight(result))
|
||||
})
|
||||
|
||||
t.Run("prevents Kleisli execution when context is cancelled", func(t *testing.T) {
|
||||
executed := false
|
||||
processUser := func(id int) ReaderResult[string] {
|
||||
return func(ctx context.Context) E.Either[error, string] {
|
||||
executed = true
|
||||
return E.Of[error]("user")
|
||||
}
|
||||
}
|
||||
|
||||
safeProcessUser := WithContextK(processUser)
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
result := safeProcessUser(123)(ctx)
|
||||
|
||||
assert.False(t, executed, "Kleisli should not be executed when context is cancelled")
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, err := E.UnwrapError(result)
|
||||
assert.Equal(t, context.Canceled, err)
|
||||
})
|
||||
|
||||
t.Run("works in Chain pipeline", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
secondExecuted := false
|
||||
|
||||
getUser := WithContextK(func(id int) ReaderResult[string] {
|
||||
return func(ctx context.Context) E.Either[error, string] {
|
||||
firstExecuted = true
|
||||
return E.Of[error]("Alice")
|
||||
}
|
||||
})
|
||||
|
||||
getOrders := WithContextK(func(name string) ReaderResult[int] {
|
||||
return func(ctx context.Context) E.Either[error, int] {
|
||||
secondExecuted = true
|
||||
return E.Of[error](5)
|
||||
}
|
||||
})
|
||||
|
||||
pipeline := F.Pipe2(
|
||||
Of(123),
|
||||
Chain(getUser),
|
||||
Chain(getOrders),
|
||||
)
|
||||
|
||||
result := pipeline(t.Context())
|
||||
|
||||
assert.True(t, firstExecuted, "first step should execute")
|
||||
assert.True(t, secondExecuted, "second step should execute")
|
||||
assert.Equal(t, E.Of[error](5), result)
|
||||
})
|
||||
|
||||
t.Run("stops pipeline on cancellation", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
secondExecuted := false
|
||||
|
||||
getUser := WithContextK(func(id int) ReaderResult[string] {
|
||||
return func(ctx context.Context) E.Either[error, string] {
|
||||
firstExecuted = true
|
||||
return E.Of[error]("Alice")
|
||||
}
|
||||
})
|
||||
|
||||
getOrders := WithContextK(func(name string) ReaderResult[int] {
|
||||
return func(ctx context.Context) E.Either[error, int] {
|
||||
secondExecuted = true
|
||||
return E.Of[error](5)
|
||||
}
|
||||
})
|
||||
|
||||
pipeline := F.Pipe2(
|
||||
Of(123),
|
||||
Chain(getUser),
|
||||
Chain(getOrders),
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
result := pipeline(ctx)
|
||||
|
||||
assert.False(t, firstExecuted, "first step should not execute")
|
||||
assert.False(t, secondExecuted, "second step should not execute")
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("respects timeout in multi-step pipeline", func(t *testing.T) {
|
||||
step1Executed := false
|
||||
step2Executed := false
|
||||
|
||||
step1 := WithContextK(func(x int) ReaderResult[int] {
|
||||
return func(ctx context.Context) E.Either[error, int] {
|
||||
step1Executed = true
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
return E.Of[error](x * 2)
|
||||
}
|
||||
})
|
||||
|
||||
step2 := WithContextK(func(x int) ReaderResult[int] {
|
||||
return func(ctx context.Context) E.Either[error, int] {
|
||||
step2Executed = true
|
||||
return E.Of[error](x + 10)
|
||||
}
|
||||
})
|
||||
|
||||
pipeline := F.Pipe2(
|
||||
Of(5),
|
||||
Chain(step1),
|
||||
Chain(step2),
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
time.Sleep(20 * time.Millisecond) // Wait for timeout
|
||||
|
||||
result := pipeline(ctx)
|
||||
|
||||
assert.False(t, step1Executed, "step1 should not execute after timeout")
|
||||
assert.False(t, step2Executed, "step2 should not execute after timeout")
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("preserves errors from Kleisli computation", func(t *testing.T) {
|
||||
testErr := errors.New("kleisli error")
|
||||
failingKleisli := func(id int) ReaderResult[string] {
|
||||
return func(ctx context.Context) E.Either[error, string] {
|
||||
return E.Left[string](testErr)
|
||||
}
|
||||
}
|
||||
|
||||
safeKleisli := WithContextK(failingKleisli)
|
||||
result := safeKleisli(123)(t.Context())
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, err := E.UnwrapError(result)
|
||||
assert.Equal(t, testErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestWithContextIntegration tests integration scenarios
|
||||
func TestWithContextIntegration(t *testing.T) {
|
||||
t.Run("WithContext in complex pipeline with multiple operations", func(t *testing.T) {
|
||||
step1Executed := false
|
||||
step2Executed := false
|
||||
step3Executed := false
|
||||
|
||||
step1 := WithContext(func(ctx context.Context) E.Either[error, int] {
|
||||
step1Executed = true
|
||||
return E.Of[error](10)
|
||||
})
|
||||
|
||||
step2 := WithContextK(func(x int) ReaderResult[int] {
|
||||
return func(ctx context.Context) E.Either[error, int] {
|
||||
step2Executed = true
|
||||
return E.Of[error](x * 2)
|
||||
}
|
||||
})
|
||||
|
||||
step3 := WithContext(func(ctx context.Context) E.Either[error, string] {
|
||||
step3Executed = true
|
||||
return E.Of[error]("done")
|
||||
})
|
||||
|
||||
pipeline := F.Pipe2(
|
||||
step1,
|
||||
Chain(step2),
|
||||
ChainTo[int](step3),
|
||||
)
|
||||
|
||||
result := pipeline(t.Context())
|
||||
|
||||
assert.True(t, step1Executed)
|
||||
assert.True(t, step2Executed)
|
||||
assert.True(t, step3Executed)
|
||||
assert.Equal(t, E.Of[error]("done"), result)
|
||||
})
|
||||
|
||||
t.Run("early cancellation prevents all subsequent operations", func(t *testing.T) {
|
||||
step1Executed := false
|
||||
step2Executed := false
|
||||
step3Executed := false
|
||||
|
||||
step1 := WithContext(func(ctx context.Context) E.Either[error, int] {
|
||||
step1Executed = true
|
||||
return E.Of[error](10)
|
||||
})
|
||||
|
||||
step2 := WithContextK(func(x int) ReaderResult[int] {
|
||||
return func(ctx context.Context) E.Either[error, int] {
|
||||
step2Executed = true
|
||||
return E.Of[error](x * 2)
|
||||
}
|
||||
})
|
||||
|
||||
step3 := WithContext(func(ctx context.Context) E.Either[error, string] {
|
||||
step3Executed = true
|
||||
return E.Of[error]("done")
|
||||
})
|
||||
|
||||
pipeline := F.Pipe2(
|
||||
step1,
|
||||
Chain(step2),
|
||||
ChainTo[int](step3),
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
result := pipeline(ctx)
|
||||
|
||||
assert.False(t, step1Executed, "no steps should execute")
|
||||
assert.False(t, step2Executed, "no steps should execute")
|
||||
assert.False(t, step3Executed, "no steps should execute")
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("WithContext with Map and Chain", func(t *testing.T) {
|
||||
computation := WithContext(func(ctx context.Context) E.Either[error, int] {
|
||||
return E.Of[error](42)
|
||||
})
|
||||
|
||||
pipeline := F.Pipe2(
|
||||
computation,
|
||||
Map(N.Mul(2)),
|
||||
Map(reader.Of[int]("result")),
|
||||
)
|
||||
|
||||
result := pipeline(t.Context())
|
||||
assert.Equal(t, E.Of[error]("result"), result)
|
||||
})
|
||||
}
|
||||
@@ -21,33 +21,299 @@ import (
|
||||
"github.com/IBM/fp-go/v2/readereither"
|
||||
)
|
||||
|
||||
// these functions curry a golang function with the context as the firsr parameter into a either reader with the context as the last parameter
|
||||
// this goes back to the advice in https://pkg.go.dev/context to put the context as a first parameter as a convention
|
||||
// Curry and Uncurry functions convert between idiomatic Go functions (with context.Context as the first parameter)
|
||||
// and functional ReaderResult/Kleisli compositions (with context.Context as the last parameter).
|
||||
//
|
||||
// This follows the Go convention from https://pkg.go.dev/context to put context as the first parameter,
|
||||
// while enabling functional composition where context is typically the last parameter.
|
||||
//
|
||||
// The curry functions transform:
|
||||
// func(context.Context, T1, T2, ...) (A, error) → func(T1) func(T2) ... ReaderResult[A]
|
||||
//
|
||||
// The uncurry functions transform:
|
||||
// func(T1) func(T2) ... ReaderResult[A] → func(context.Context, T1, T2, ...) (A, error)
|
||||
|
||||
// Curry0 converts a Go function with context and no additional parameters into a ReaderResult.
|
||||
// This is useful for adapting context-aware functions to the ReaderResult monad.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The return type of the function
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that takes a context and returns a value and error
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderResult that wraps the function
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Idiomatic Go function
|
||||
// getConfig := func(ctx context.Context) (Config, error) {
|
||||
// // Check context cancellation
|
||||
// if ctx.Err() != nil {
|
||||
// return Config{}, ctx.Err()
|
||||
// }
|
||||
// return Config{Value: 42}, nil
|
||||
// }
|
||||
//
|
||||
// // Convert to ReaderResult for functional composition
|
||||
// configRR := readerresult.Curry0(getConfig)
|
||||
// result := configRR(t.Context()) // Right(Config{Value: 42})
|
||||
//
|
||||
//go:inline
|
||||
func Curry0[A any](f func(context.Context) (A, error)) ReaderResult[A] {
|
||||
return readereither.Curry0(f)
|
||||
}
|
||||
|
||||
// Curry1 converts a Go function with context and one parameter into a Kleisli arrow.
|
||||
// This enables functional composition of single-parameter functions.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T1: The type of the first parameter
|
||||
// - A: The return type of the function
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that takes a context and one parameter, returning a value and error
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that can be composed with other ReaderResult operations
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Idiomatic Go function
|
||||
// getUserByID := func(ctx context.Context, id int) (User, error) {
|
||||
// if ctx.Err() != nil {
|
||||
// return User{}, ctx.Err()
|
||||
// }
|
||||
// return User{ID: id, Name: "Alice"}, nil
|
||||
// }
|
||||
//
|
||||
// // Convert to Kleisli for functional composition
|
||||
// getUserKleisli := readerresult.Curry1(getUserByID)
|
||||
//
|
||||
// // Use in a pipeline
|
||||
// pipeline := F.Pipe1(
|
||||
// readerresult.Of(123),
|
||||
// readerresult.Chain(getUserKleisli),
|
||||
// )
|
||||
// result := pipeline(t.Context()) // Right(User{ID: 123, Name: "Alice"})
|
||||
//
|
||||
//go:inline
|
||||
func Curry1[T1, A any](f func(context.Context, T1) (A, error)) Kleisli[T1, A] {
|
||||
return readereither.Curry1(f)
|
||||
}
|
||||
|
||||
// Curry2 converts a Go function with context and two parameters into a curried function.
|
||||
// This enables partial application and functional composition of two-parameter functions.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T1: The type of the first parameter
|
||||
// - T2: The type of the second parameter
|
||||
// - A: The return type of the function
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that takes a context and two parameters, returning a value and error
|
||||
//
|
||||
// Returns:
|
||||
// - A curried function that takes T1 and returns a Kleisli arrow for T2
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Idiomatic Go function
|
||||
// updateUser := func(ctx context.Context, id int, name string) (User, error) {
|
||||
// if ctx.Err() != nil {
|
||||
// return User{}, ctx.Err()
|
||||
// }
|
||||
// return User{ID: id, Name: name}, nil
|
||||
// }
|
||||
//
|
||||
// // Convert to curried form
|
||||
// updateUserCurried := readerresult.Curry2(updateUser)
|
||||
//
|
||||
// // Partial application
|
||||
// updateUser123 := updateUserCurried(123)
|
||||
//
|
||||
// // Use in a pipeline
|
||||
// pipeline := F.Pipe1(
|
||||
// readerresult.Of("Bob"),
|
||||
// readerresult.Chain(updateUser123),
|
||||
// )
|
||||
// result := pipeline(t.Context()) // Right(User{ID: 123, Name: "Bob"})
|
||||
//
|
||||
//go:inline
|
||||
func Curry2[T1, T2, A any](f func(context.Context, T1, T2) (A, error)) func(T1) Kleisli[T2, A] {
|
||||
return readereither.Curry2(f)
|
||||
}
|
||||
|
||||
// Curry3 converts a Go function with context and three parameters into a curried function.
|
||||
// This enables partial application and functional composition of three-parameter functions.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T1: The type of the first parameter
|
||||
// - T2: The type of the second parameter
|
||||
// - T3: The type of the third parameter
|
||||
// - A: The return type of the function
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that takes a context and three parameters, returning a value and error
|
||||
//
|
||||
// Returns:
|
||||
// - A curried function that takes T1, T2, and returns a Kleisli arrow for T3
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Idiomatic Go function
|
||||
// createOrder := func(ctx context.Context, userID int, productID int, quantity int) (Order, error) {
|
||||
// if ctx.Err() != nil {
|
||||
// return Order{}, ctx.Err()
|
||||
// }
|
||||
// return Order{UserID: userID, ProductID: productID, Quantity: quantity}, nil
|
||||
// }
|
||||
//
|
||||
// // Convert to curried form
|
||||
// createOrderCurried := readerresult.Curry3(createOrder)
|
||||
//
|
||||
// // Partial application
|
||||
// createOrderForUser := createOrderCurried(123)
|
||||
// createOrderForProduct := createOrderForUser(456)
|
||||
//
|
||||
// // Use in a pipeline
|
||||
// pipeline := F.Pipe1(
|
||||
// readerresult.Of(2),
|
||||
// readerresult.Chain(createOrderForProduct),
|
||||
// )
|
||||
// result := pipeline(t.Context()) // Right(Order{UserID: 123, ProductID: 456, Quantity: 2})
|
||||
//
|
||||
//go:inline
|
||||
func Curry3[T1, T2, T3, A any](f func(context.Context, T1, T2, T3) (A, error)) func(T1) func(T2) Kleisli[T3, A] {
|
||||
return readereither.Curry3(f)
|
||||
}
|
||||
|
||||
// Uncurry1 converts a Kleisli arrow back into an idiomatic Go function with context as the first parameter.
|
||||
// This is useful for interfacing with code that expects standard Go function signatures.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T1: The type of the parameter
|
||||
// - A: The return type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Kleisli arrow
|
||||
//
|
||||
// Returns:
|
||||
// - A Go function with context as the first parameter
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Kleisli arrow
|
||||
// getUserKleisli := func(id int) readerresult.ReaderResult[User] {
|
||||
// return func(ctx context.Context) result.Result[User] {
|
||||
// if ctx.Err() != nil {
|
||||
// return result.Error[User](ctx.Err())
|
||||
// }
|
||||
// return result.Of(User{ID: id, Name: "Alice"})
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Convert back to idiomatic Go function
|
||||
// getUserByID := readerresult.Uncurry1(getUserKleisli)
|
||||
//
|
||||
// // Use as a normal Go function
|
||||
// user, err := getUserByID(t.Context(), 123)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// fmt.Println(user.Name) // "Alice"
|
||||
//
|
||||
//go:inline
|
||||
func Uncurry1[T1, A any](f Kleisli[T1, A]) func(context.Context, T1) (A, error) {
|
||||
return readereither.Uncurry1(f)
|
||||
}
|
||||
|
||||
// Uncurry2 converts a curried function back into an idiomatic Go function with context as the first parameter.
|
||||
// This is useful for interfacing with code that expects standard Go function signatures.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T1: The type of the first parameter
|
||||
// - T2: The type of the second parameter
|
||||
// - A: The return type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A curried function
|
||||
//
|
||||
// Returns:
|
||||
// - A Go function with context as the first parameter
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Curried function
|
||||
// updateUserCurried := func(id int) func(name string) readerresult.ReaderResult[User] {
|
||||
// return func(name string) readerresult.ReaderResult[User] {
|
||||
// return func(ctx context.Context) result.Result[User] {
|
||||
// if ctx.Err() != nil {
|
||||
// return result.Error[User](ctx.Err())
|
||||
// }
|
||||
// return result.Of(User{ID: id, Name: name})
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Convert back to idiomatic Go function
|
||||
// updateUser := readerresult.Uncurry2(updateUserCurried)
|
||||
//
|
||||
// // Use as a normal Go function
|
||||
// user, err := updateUser(t.Context(), 123, "Bob")
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// fmt.Println(user.Name) // "Bob"
|
||||
//
|
||||
//go:inline
|
||||
func Uncurry2[T1, T2, A any](f func(T1) Kleisli[T2, A]) func(context.Context, T1, T2) (A, error) {
|
||||
return readereither.Uncurry2(f)
|
||||
}
|
||||
|
||||
// Uncurry3 converts a curried function back into an idiomatic Go function with context as the first parameter.
|
||||
// This is useful for interfacing with code that expects standard Go function signatures.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T1: The type of the first parameter
|
||||
// - T2: The type of the second parameter
|
||||
// - T3: The type of the third parameter
|
||||
// - A: The return type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A curried function
|
||||
//
|
||||
// Returns:
|
||||
// - A Go function with context as the first parameter
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Curried function
|
||||
// createOrderCurried := func(userID int) func(productID int) func(quantity int) readerresult.ReaderResult[Order] {
|
||||
// return func(productID int) func(quantity int) readerresult.ReaderResult[Order] {
|
||||
// return func(quantity int) readerresult.ReaderResult[Order] {
|
||||
// return func(ctx context.Context) result.Result[Order] {
|
||||
// if ctx.Err() != nil {
|
||||
// return result.Error[Order](ctx.Err())
|
||||
// }
|
||||
// return result.Of(Order{UserID: userID, ProductID: productID, Quantity: quantity})
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Convert back to idiomatic Go function
|
||||
// createOrder := readerresult.Uncurry3(createOrderCurried)
|
||||
//
|
||||
// // Use as a normal Go function
|
||||
// order, err := createOrder(t.Context(), 123, 456, 2)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// fmt.Printf("Order: User=%d, Product=%d, Qty=%d\n", order.UserID, order.ProductID, order.Quantity)
|
||||
//
|
||||
//go:inline
|
||||
func Uncurry3[T1, T2, T3, A any](f func(T1) func(T2) Kleisli[T3, A]) func(context.Context, T1, T2, T3) (A, error) {
|
||||
return readereither.Uncurry3(f)
|
||||
}
|
||||
|
||||
564
v2/context/readerresult/curry_test.go
Normal file
564
v2/context/readerresult/curry_test.go
Normal file
@@ -0,0 +1,564 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestCurry0 tests the Curry0 function
|
||||
func TestCurry0(t *testing.T) {
|
||||
t.Run("converts Go function to ReaderResult on success", func(t *testing.T) {
|
||||
// Idiomatic Go function
|
||||
getConfig := func(ctx context.Context) (int, error) {
|
||||
return 42, nil
|
||||
}
|
||||
|
||||
// Convert to ReaderResult
|
||||
configRR := Curry0(getConfig)
|
||||
result := configRR(t.Context())
|
||||
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
})
|
||||
|
||||
t.Run("converts Go function to ReaderResult on error", func(t *testing.T) {
|
||||
testErr := errors.New("config error")
|
||||
getConfig := func(ctx context.Context) (int, error) {
|
||||
return 0, testErr
|
||||
}
|
||||
|
||||
configRR := Curry0(getConfig)
|
||||
result := configRR(t.Context())
|
||||
|
||||
assert.Equal(t, E.Left[int](testErr), result)
|
||||
})
|
||||
|
||||
t.Run("respects context cancellation", func(t *testing.T) {
|
||||
getConfig := func(ctx context.Context) (int, error) {
|
||||
if ctx.Err() != nil {
|
||||
return 0, ctx.Err()
|
||||
}
|
||||
return 42, nil
|
||||
}
|
||||
|
||||
configRR := Curry0(getConfig)
|
||||
|
||||
// Test with cancelled context
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
result := configRR(ctx)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("can be used in functional composition", func(t *testing.T) {
|
||||
getConfig := func(ctx context.Context) (int, error) {
|
||||
return 42, nil
|
||||
}
|
||||
|
||||
pipeline := F.Pipe1(
|
||||
Curry0(getConfig),
|
||||
Map(func(x int) string {
|
||||
return "value"
|
||||
}),
|
||||
)
|
||||
|
||||
result := pipeline(t.Context())
|
||||
assert.True(t, E.IsRight(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestCurry1 tests the Curry1 function
|
||||
func TestCurry1(t *testing.T) {
|
||||
t.Run("converts Go function to Kleisli on success", func(t *testing.T) {
|
||||
getUserByID := func(ctx context.Context, id int) (string, error) {
|
||||
return "Alice", nil
|
||||
}
|
||||
|
||||
getUserKleisli := Curry1(getUserByID)
|
||||
|
||||
// Use in a pipeline
|
||||
pipeline := F.Pipe1(
|
||||
Of(123),
|
||||
Chain(getUserKleisli),
|
||||
)
|
||||
|
||||
result := pipeline(t.Context())
|
||||
assert.Equal(t, E.Of[error]("Alice"), result)
|
||||
})
|
||||
|
||||
t.Run("converts Go function to Kleisli on error", func(t *testing.T) {
|
||||
testErr := errors.New("user not found")
|
||||
getUserByID := func(ctx context.Context, id int) (string, error) {
|
||||
return "", testErr
|
||||
}
|
||||
|
||||
getUserKleisli := Curry1(getUserByID)
|
||||
result := getUserKleisli(123)(t.Context())
|
||||
|
||||
assert.Equal(t, E.Left[string](testErr), result)
|
||||
})
|
||||
|
||||
t.Run("respects context cancellation", func(t *testing.T) {
|
||||
getUserByID := func(ctx context.Context, id int) (string, error) {
|
||||
if ctx.Err() != nil {
|
||||
return "", ctx.Err()
|
||||
}
|
||||
return "Alice", nil
|
||||
}
|
||||
|
||||
getUserKleisli := Curry1(getUserByID)
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
result := getUserKleisli(123)(ctx)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("can be composed with other operations", func(t *testing.T) {
|
||||
getUserByID := func(ctx context.Context, id int) (string, error) {
|
||||
return "Alice", nil
|
||||
}
|
||||
|
||||
pipeline := F.Pipe2(
|
||||
Of(123),
|
||||
Chain(Curry1(getUserByID)),
|
||||
Map(func(name string) int {
|
||||
return len(name)
|
||||
}),
|
||||
)
|
||||
|
||||
result := pipeline(t.Context())
|
||||
assert.Equal(t, E.Of[error](5), result) // len("Alice") = 5
|
||||
})
|
||||
}
|
||||
|
||||
// TestCurry2 tests the Curry2 function
|
||||
func TestCurry2(t *testing.T) {
|
||||
t.Run("converts Go function to curried form on success", func(t *testing.T) {
|
||||
updateUser := func(ctx context.Context, id int, name string) (string, error) {
|
||||
return name, nil
|
||||
}
|
||||
|
||||
updateUserCurried := Curry2(updateUser)
|
||||
|
||||
// Partial application
|
||||
updateUser123 := updateUserCurried(123)
|
||||
|
||||
// Use in a pipeline
|
||||
pipeline := F.Pipe1(
|
||||
Of("Bob"),
|
||||
Chain(updateUser123),
|
||||
)
|
||||
|
||||
result := pipeline(t.Context())
|
||||
assert.Equal(t, E.Of[error]("Bob"), result)
|
||||
})
|
||||
|
||||
t.Run("converts Go function to curried form on error", func(t *testing.T) {
|
||||
testErr := errors.New("update failed")
|
||||
updateUser := func(ctx context.Context, id int, name string) (string, error) {
|
||||
return "", testErr
|
||||
}
|
||||
|
||||
updateUserCurried := Curry2(updateUser)
|
||||
result := updateUserCurried(123)("Bob")(t.Context())
|
||||
|
||||
assert.Equal(t, E.Left[string](testErr), result)
|
||||
})
|
||||
|
||||
t.Run("supports partial application", func(t *testing.T) {
|
||||
concat := func(ctx context.Context, a string, b string) (string, error) {
|
||||
return a + b, nil
|
||||
}
|
||||
|
||||
concatCurried := Curry2(concat)
|
||||
|
||||
// Partial application
|
||||
prependHello := concatCurried("Hello, ")
|
||||
|
||||
result1 := prependHello("World")(t.Context())
|
||||
result2 := prependHello("Alice")(t.Context())
|
||||
|
||||
assert.Equal(t, E.Of[error]("Hello, World"), result1)
|
||||
assert.Equal(t, E.Of[error]("Hello, Alice"), result2)
|
||||
})
|
||||
|
||||
t.Run("respects context cancellation", func(t *testing.T) {
|
||||
updateUser := func(ctx context.Context, id int, name string) (string, error) {
|
||||
if ctx.Err() != nil {
|
||||
return "", ctx.Err()
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
||||
updateUserCurried := Curry2(updateUser)
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
result := updateUserCurried(123)("Bob")(ctx)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestCurry3 tests the Curry3 function
|
||||
func TestCurry3(t *testing.T) {
|
||||
t.Run("converts Go function to curried form on success", func(t *testing.T) {
|
||||
createOrder := func(ctx context.Context, userID int, productID int, quantity int) (int, error) {
|
||||
return userID + productID + quantity, nil
|
||||
}
|
||||
|
||||
createOrderCurried := Curry3(createOrder)
|
||||
|
||||
// Partial application
|
||||
createOrderForUser := createOrderCurried(100)
|
||||
createOrderForProduct := createOrderForUser(200)
|
||||
|
||||
// Use in a pipeline
|
||||
pipeline := F.Pipe1(
|
||||
Of(3),
|
||||
Chain(createOrderForProduct),
|
||||
)
|
||||
|
||||
result := pipeline(t.Context())
|
||||
assert.Equal(t, E.Of[error](303), result) // 100 + 200 + 3
|
||||
})
|
||||
|
||||
t.Run("converts Go function to curried form on error", func(t *testing.T) {
|
||||
testErr := errors.New("order creation failed")
|
||||
createOrder := func(ctx context.Context, userID int, productID int, quantity int) (int, error) {
|
||||
return 0, testErr
|
||||
}
|
||||
|
||||
createOrderCurried := Curry3(createOrder)
|
||||
result := createOrderCurried(100)(200)(3)(t.Context())
|
||||
|
||||
assert.Equal(t, E.Left[int](testErr), result)
|
||||
})
|
||||
|
||||
t.Run("supports multiple levels of partial application", func(t *testing.T) {
|
||||
sum3 := func(ctx context.Context, a int, b int, c int) (int, error) {
|
||||
return a + b + c, nil
|
||||
}
|
||||
|
||||
sum3Curried := Curry3(sum3)
|
||||
|
||||
// First level partial application
|
||||
add10 := sum3Curried(10)
|
||||
|
||||
// Second level partial application
|
||||
add10And20 := add10(20)
|
||||
|
||||
result1 := add10And20(5)(t.Context())
|
||||
result2 := add10And20(15)(t.Context())
|
||||
|
||||
assert.Equal(t, E.Of[error](35), result1) // 10 + 20 + 5
|
||||
assert.Equal(t, E.Of[error](45), result2) // 10 + 20 + 15
|
||||
})
|
||||
|
||||
t.Run("respects context cancellation", func(t *testing.T) {
|
||||
createOrder := func(ctx context.Context, userID int, productID int, quantity int) (int, error) {
|
||||
if ctx.Err() != nil {
|
||||
return 0, ctx.Err()
|
||||
}
|
||||
return userID + productID + quantity, nil
|
||||
}
|
||||
|
||||
createOrderCurried := Curry3(createOrder)
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
result := createOrderCurried(100)(200)(3)(ctx)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestUncurry1 tests the Uncurry1 function
|
||||
func TestUncurry1(t *testing.T) {
|
||||
t.Run("converts Kleisli back to Go function on success", func(t *testing.T) {
|
||||
getUserKleisli := func(id int) ReaderResult[string] {
|
||||
return func(ctx context.Context) E.Either[error, string] {
|
||||
return E.Of[error]("Alice")
|
||||
}
|
||||
}
|
||||
|
||||
getUserByID := Uncurry1(getUserKleisli)
|
||||
|
||||
user, err := getUserByID(t.Context(), 123)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", user)
|
||||
})
|
||||
|
||||
t.Run("converts Kleisli back to Go function on error", func(t *testing.T) {
|
||||
testErr := errors.New("user not found")
|
||||
getUserKleisli := func(id int) ReaderResult[string] {
|
||||
return func(ctx context.Context) E.Either[error, string] {
|
||||
return E.Left[string](testErr)
|
||||
}
|
||||
}
|
||||
|
||||
getUserByID := Uncurry1(getUserKleisli)
|
||||
|
||||
user, err := getUserByID(t.Context(), 123)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, testErr, err)
|
||||
assert.Equal(t, "", user)
|
||||
})
|
||||
|
||||
t.Run("respects context in uncurried function", func(t *testing.T) {
|
||||
getUserKleisli := func(id int) ReaderResult[string] {
|
||||
return func(ctx context.Context) E.Either[error, string] {
|
||||
if ctx.Err() != nil {
|
||||
return E.Left[string](ctx.Err())
|
||||
}
|
||||
return E.Of[error]("Alice")
|
||||
}
|
||||
}
|
||||
|
||||
getUserByID := Uncurry1(getUserKleisli)
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
user, err := getUserByID(ctx, 123)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "", user)
|
||||
})
|
||||
|
||||
t.Run("round-trip with Curry1", func(t *testing.T) {
|
||||
// Original Go function
|
||||
original := func(ctx context.Context, id int) (string, error) {
|
||||
return "Alice", nil
|
||||
}
|
||||
|
||||
// Curry then uncurry
|
||||
roundTrip := Uncurry1(Curry1(original))
|
||||
|
||||
user, err := roundTrip(t.Context(), 123)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", user)
|
||||
})
|
||||
}
|
||||
|
||||
// TestUncurry2 tests the Uncurry2 function
|
||||
func TestUncurry2(t *testing.T) {
|
||||
t.Run("converts curried function back to Go function on success", func(t *testing.T) {
|
||||
updateUserCurried := func(id int) func(name string) ReaderResult[string] {
|
||||
return func(name string) ReaderResult[string] {
|
||||
return func(ctx context.Context) E.Either[error, string] {
|
||||
return E.Of[error](name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateUser := Uncurry2(updateUserCurried)
|
||||
|
||||
result, err := updateUser(t.Context(), 123, "Bob")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Bob", result)
|
||||
})
|
||||
|
||||
t.Run("converts curried function back to Go function on error", func(t *testing.T) {
|
||||
testErr := errors.New("update failed")
|
||||
updateUserCurried := func(id int) func(name string) ReaderResult[string] {
|
||||
return func(name string) ReaderResult[string] {
|
||||
return func(ctx context.Context) E.Either[error, string] {
|
||||
return E.Left[string](testErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateUser := Uncurry2(updateUserCurried)
|
||||
|
||||
result, err := updateUser(t.Context(), 123, "Bob")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, testErr, err)
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
|
||||
t.Run("respects context in uncurried function", func(t *testing.T) {
|
||||
updateUserCurried := func(id int) func(name string) ReaderResult[string] {
|
||||
return func(name string) ReaderResult[string] {
|
||||
return func(ctx context.Context) E.Either[error, string] {
|
||||
if ctx.Err() != nil {
|
||||
return E.Left[string](ctx.Err())
|
||||
}
|
||||
return E.Of[error](name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateUser := Uncurry2(updateUserCurried)
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
result, err := updateUser(ctx, 123, "Bob")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
|
||||
t.Run("round-trip with Curry2", func(t *testing.T) {
|
||||
// Original Go function
|
||||
original := func(ctx context.Context, a string, b string) (string, error) {
|
||||
return a + b, nil
|
||||
}
|
||||
|
||||
// Curry then uncurry
|
||||
roundTrip := Uncurry2(Curry2(original))
|
||||
|
||||
result, err := roundTrip(t.Context(), "Hello, ", "World")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Hello, World", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestUncurry3 tests the Uncurry3 function
|
||||
func TestUncurry3(t *testing.T) {
|
||||
t.Run("converts curried function back to Go function on success", func(t *testing.T) {
|
||||
createOrderCurried := func(userID int) func(productID int) func(quantity int) ReaderResult[int] {
|
||||
return func(productID int) func(quantity int) ReaderResult[int] {
|
||||
return func(quantity int) ReaderResult[int] {
|
||||
return func(ctx context.Context) E.Either[error, int] {
|
||||
return E.Of[error](userID + productID + quantity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createOrder := Uncurry3(createOrderCurried)
|
||||
|
||||
result, err := createOrder(t.Context(), 100, 200, 3)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 303, result) // 100 + 200 + 3
|
||||
})
|
||||
|
||||
t.Run("converts curried function back to Go function on error", func(t *testing.T) {
|
||||
testErr := errors.New("order creation failed")
|
||||
createOrderCurried := func(userID int) func(productID int) func(quantity int) ReaderResult[int] {
|
||||
return func(productID int) func(quantity int) ReaderResult[int] {
|
||||
return func(quantity int) ReaderResult[int] {
|
||||
return func(ctx context.Context) E.Either[error, int] {
|
||||
return E.Left[int](testErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createOrder := Uncurry3(createOrderCurried)
|
||||
|
||||
result, err := createOrder(t.Context(), 100, 200, 3)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, testErr, err)
|
||||
assert.Equal(t, 0, result)
|
||||
})
|
||||
|
||||
t.Run("respects context in uncurried function", func(t *testing.T) {
|
||||
createOrderCurried := func(userID int) func(productID int) func(quantity int) ReaderResult[int] {
|
||||
return func(productID int) func(quantity int) ReaderResult[int] {
|
||||
return func(quantity int) ReaderResult[int] {
|
||||
return func(ctx context.Context) E.Either[error, int] {
|
||||
if ctx.Err() != nil {
|
||||
return E.Left[int](ctx.Err())
|
||||
}
|
||||
return E.Of[error](userID + productID + quantity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createOrder := Uncurry3(createOrderCurried)
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
result, err := createOrder(ctx, 100, 200, 3)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 0, result)
|
||||
})
|
||||
|
||||
t.Run("round-trip with Curry3", func(t *testing.T) {
|
||||
// Original Go function
|
||||
original := func(ctx context.Context, a int, b int, c int) (int, error) {
|
||||
return a + b + c, nil
|
||||
}
|
||||
|
||||
// Curry then uncurry
|
||||
roundTrip := Uncurry3(Curry3(original))
|
||||
|
||||
result, err := roundTrip(t.Context(), 10, 20, 5)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 35, result) // 10 + 20 + 5
|
||||
})
|
||||
}
|
||||
|
||||
// TestCurryUncurryIntegration tests integration between curry and uncurry functions
|
||||
func TestCurryUncurryIntegration(t *testing.T) {
|
||||
t.Run("Curry1 and Uncurry1 are inverses", func(t *testing.T) {
|
||||
original := func(ctx context.Context, x int) (int, error) {
|
||||
return x * 2, nil
|
||||
}
|
||||
|
||||
// Curry then uncurry should give back equivalent function
|
||||
roundTrip := Uncurry1(Curry1(original))
|
||||
|
||||
result1, err1 := original(t.Context(), 21)
|
||||
result2, err2 := roundTrip(t.Context(), 21)
|
||||
|
||||
assert.NoError(t, err1)
|
||||
assert.NoError(t, err2)
|
||||
assert.Equal(t, result1, result2)
|
||||
})
|
||||
|
||||
t.Run("Curry2 and Uncurry2 are inverses", func(t *testing.T) {
|
||||
original := func(ctx context.Context, x int, y int) (int, error) {
|
||||
return x + y, nil
|
||||
}
|
||||
|
||||
roundTrip := Uncurry2(Curry2(original))
|
||||
|
||||
result1, err1 := original(t.Context(), 10, 20)
|
||||
result2, err2 := roundTrip(t.Context(), 10, 20)
|
||||
|
||||
assert.NoError(t, err1)
|
||||
assert.NoError(t, err2)
|
||||
assert.Equal(t, result1, result2)
|
||||
})
|
||||
|
||||
t.Run("Curry3 and Uncurry3 are inverses", func(t *testing.T) {
|
||||
original := func(ctx context.Context, x int, y int, z int) (int, error) {
|
||||
return x * y * z, nil
|
||||
}
|
||||
|
||||
roundTrip := Uncurry3(Curry3(original))
|
||||
|
||||
result1, err1 := original(t.Context(), 2, 3, 4)
|
||||
result2, err2 := roundTrip(t.Context(), 2, 3, 4)
|
||||
|
||||
assert.NoError(t, err1)
|
||||
assert.NoError(t, err2)
|
||||
assert.Equal(t, result1, result2)
|
||||
})
|
||||
}
|
||||
@@ -43,7 +43,7 @@ import (
|
||||
// onNegative := func(n int) error { return fmt.Errorf("%d is not positive", n) }
|
||||
//
|
||||
// filter := readerresult.FilterOrElse(isPositive, onNegative)
|
||||
// result := filter(readerresult.Right(42))(context.Background())
|
||||
// result := filter(readerresult.Right(42))(t.Context())
|
||||
//
|
||||
//go:inline
|
||||
func FilterOrElse[A any](pred Predicate[A], onFalse func(A) error) Operator[A, A] {
|
||||
|
||||
@@ -63,7 +63,7 @@ import (
|
||||
// // Sequenced: takes context first, then Database
|
||||
// sequenced := SequenceReader(original)
|
||||
//
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// db := Database{ConnectionString: "localhost:5432"}
|
||||
//
|
||||
// // Apply context first to get a function that takes database
|
||||
@@ -135,7 +135,7 @@ func SequenceReader[R, A any](ma ReaderResult[Reader[R, A]]) reader.Kleisli[cont
|
||||
//
|
||||
// // Now we can provide Config first, then context
|
||||
// cfg := Config{MaxRetries: 3}
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
//
|
||||
// result := flipped(cfg)(ctx)
|
||||
// // result is Result[string] containing "Value: 42, MaxRetries: 3"
|
||||
|
||||
@@ -96,7 +96,7 @@ func curriedLog(
|
||||
// logDebug := SLogWithCallback[User](slog.LevelDebug, getLogger, "User data")
|
||||
//
|
||||
// // Use in a pipeline
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// user := result.Of(User{ID: 123, Name: "Alice"})
|
||||
// logged := logDebug(user)(ctx) // Logs: level=DEBUG msg="User data" value={ID:123 Name:Alice}
|
||||
// // logged still contains the User value
|
||||
@@ -149,7 +149,7 @@ func SLogWithCallback[A any](
|
||||
//
|
||||
// Example - Logging a successful computation:
|
||||
//
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
//
|
||||
// // Simple value logging
|
||||
// res := result.Of(42)
|
||||
@@ -172,7 +172,7 @@ func SLogWithCallback[A any](
|
||||
// return result.Of(fmt.Sprintf("Processed: %s", user.Name))
|
||||
// }
|
||||
//
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
//
|
||||
// // Log at each step
|
||||
// userResult := fetchUser(123)
|
||||
@@ -195,7 +195,7 @@ func SLogWithCallback[A any](
|
||||
//
|
||||
// // Set up a custom logger in the context
|
||||
// logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||
// ctx := logging.WithLogger(logger)(context.Background())
|
||||
// ctx := logging.WithLogger(logger)(t.Context())
|
||||
//
|
||||
// res := result.Of("important data")
|
||||
// logged := SLog[string]("Critical operation")(res)(ctx)
|
||||
|
||||
@@ -37,7 +37,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)
|
||||
@@ -59,7 +59,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
|
||||
@@ -83,7 +83,7 @@ func TestSLogInPipeline(t *testing.T) {
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// SLog takes a Result[A] and returns ReaderResult[A]
|
||||
// So we need to start with a Result, apply SLog, then execute with context
|
||||
@@ -104,7 +104,7 @@ func TestSLogWithContextLogger(t *testing.T) {
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
|
||||
ctx := logging.WithLogger(contextLogger)(context.Background())
|
||||
ctx := logging.WithLogger(contextLogger)(t.Context())
|
||||
|
||||
res1 := result.Of("test value")
|
||||
logged := SLog[string]("Context logger test")(res1)(ctx)
|
||||
@@ -126,7 +126,7 @@ func TestSLogDisabled(t *testing.T) {
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
res1 := result.Of(42)
|
||||
logged := SLog[int]("This should not be logged")(res1)(ctx)
|
||||
@@ -152,7 +152,7 @@ func TestSLogWithStruct(t *testing.T) {
|
||||
Name string
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
user := User{ID: 123, Name: "Alice"}
|
||||
|
||||
res1 := result.Of(user)
|
||||
@@ -177,7 +177,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)
|
||||
@@ -202,7 +202,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
|
||||
@@ -227,7 +227,7 @@ func TestSLogChainedOperations(t *testing.T) {
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// First log step 1
|
||||
res1 := result.Of(5)
|
||||
@@ -255,7 +255,7 @@ func TestSLogPreservesError(t *testing.T) {
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
testErr := errors.New("original error")
|
||||
|
||||
res1 := result.Left[int](testErr)
|
||||
@@ -280,7 +280,7 @@ func TestSLogMultipleValues(t *testing.T) {
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with different types
|
||||
intRes := SLog[int]("Integer")(result.Of(42))(ctx)
|
||||
|
||||
@@ -41,7 +41,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)
|
||||
})
|
||||
@@ -63,7 +63,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)
|
||||
})
|
||||
@@ -85,7 +85,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)
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -38,7 +39,7 @@ func TestMapTo(t *testing.T) {
|
||||
resultReader := toDone(originalReader)
|
||||
|
||||
// Execute the resulting reader
|
||||
result := resultReader(context.Background())
|
||||
result := resultReader(t.Context())
|
||||
|
||||
// Verify the constant value is returned
|
||||
assert.Equal(t, E.Of[error]("done"), result)
|
||||
@@ -58,7 +59,7 @@ func TestMapTo(t *testing.T) {
|
||||
MapTo[int]("complete"),
|
||||
)
|
||||
|
||||
result := pipeline(context.Background())
|
||||
result := pipeline(t.Context())
|
||||
|
||||
assert.Equal(t, E.Of[error]("complete"), result)
|
||||
assert.True(t, executed, "original reader should be executed in pipeline")
|
||||
@@ -72,7 +73,7 @@ func TestMapTo(t *testing.T) {
|
||||
}
|
||||
|
||||
resultReader := MapTo[int](true)(readerWithSideEffect)
|
||||
result := resultReader(context.Background())
|
||||
result := resultReader(t.Context())
|
||||
|
||||
assert.Equal(t, E.Of[error](true), result)
|
||||
assert.True(t, sideEffectOccurred, "side effect should occur")
|
||||
@@ -87,7 +88,7 @@ func TestMapTo(t *testing.T) {
|
||||
}
|
||||
|
||||
resultReader := MapTo[int]("done")(failingReader)
|
||||
result := resultReader(context.Background())
|
||||
result := resultReader(t.Context())
|
||||
|
||||
assert.Equal(t, E.Left[string](testErr), result)
|
||||
assert.True(t, executed, "failing reader should still be executed")
|
||||
@@ -106,7 +107,7 @@ func TestMonadMapTo(t *testing.T) {
|
||||
resultReader := MonadMapTo(originalReader, "done")
|
||||
|
||||
// Execute the resulting reader
|
||||
result := resultReader(context.Background())
|
||||
result := resultReader(t.Context())
|
||||
|
||||
// Verify the constant value is returned
|
||||
assert.Equal(t, E.Of[error]("done"), result)
|
||||
@@ -122,7 +123,7 @@ func TestMonadMapTo(t *testing.T) {
|
||||
}
|
||||
|
||||
resultReader := MonadMapTo(complexReader, 42)
|
||||
result := resultReader(context.Background())
|
||||
result := resultReader(t.Context())
|
||||
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
assert.True(t, computationExecuted, "complex computation should be executed")
|
||||
@@ -137,7 +138,7 @@ func TestMonadMapTo(t *testing.T) {
|
||||
}
|
||||
|
||||
resultReader := MonadMapTo(failingReader, 99)
|
||||
result := resultReader(context.Background())
|
||||
result := resultReader(t.Context())
|
||||
|
||||
assert.Equal(t, E.Left[int](testErr), result)
|
||||
assert.True(t, executed, "failing reader should still be executed")
|
||||
@@ -164,7 +165,7 @@ func TestChainTo(t *testing.T) {
|
||||
resultReader := thenSecond(firstReader)
|
||||
|
||||
// Execute the resulting reader
|
||||
result := resultReader(context.Background())
|
||||
result := resultReader(t.Context())
|
||||
|
||||
// Verify the second reader's result is returned
|
||||
assert.Equal(t, E.Of[error]("result"), result)
|
||||
@@ -192,7 +193,7 @@ func TestChainTo(t *testing.T) {
|
||||
ChainTo[int](step2),
|
||||
)
|
||||
|
||||
result := pipeline(context.Background())
|
||||
result := pipeline(t.Context())
|
||||
|
||||
assert.Equal(t, E.Of[error]("complete"), result)
|
||||
assert.True(t, firstExecuted, "first reader should be executed in pipeline")
|
||||
@@ -211,7 +212,7 @@ func TestChainTo(t *testing.T) {
|
||||
}
|
||||
|
||||
resultReader := ChainTo[int](secondReader)(readerWithSideEffect)
|
||||
result := resultReader(context.Background())
|
||||
result := resultReader(t.Context())
|
||||
|
||||
assert.Equal(t, E.Of[error](true), result)
|
||||
assert.True(t, sideEffectOccurred, "side effect should occur in first reader")
|
||||
@@ -233,7 +234,7 @@ func TestChainTo(t *testing.T) {
|
||||
}
|
||||
|
||||
resultReader := ChainTo[int](secondReader)(failingReader)
|
||||
result := resultReader(context.Background())
|
||||
result := resultReader(t.Context())
|
||||
|
||||
assert.Equal(t, E.Left[string](testErr), result)
|
||||
assert.True(t, firstExecuted, "first reader should be executed")
|
||||
@@ -260,7 +261,7 @@ func TestMonadChainTo(t *testing.T) {
|
||||
resultReader := MonadChainTo(firstReader, secondReader)
|
||||
|
||||
// Execute the resulting reader
|
||||
result := resultReader(context.Background())
|
||||
result := resultReader(t.Context())
|
||||
|
||||
// Verify the second reader's result is returned
|
||||
assert.Equal(t, E.Of[error]("result"), result)
|
||||
@@ -284,7 +285,7 @@ func TestMonadChainTo(t *testing.T) {
|
||||
}
|
||||
|
||||
resultReader := MonadChainTo(complexFirstReader, secondReader)
|
||||
result := resultReader(context.Background())
|
||||
result := resultReader(t.Context())
|
||||
|
||||
assert.Equal(t, E.Of[error]("done"), result)
|
||||
assert.True(t, firstExecuted, "complex first computation should be executed")
|
||||
@@ -307,7 +308,7 @@ func TestMonadChainTo(t *testing.T) {
|
||||
}
|
||||
|
||||
resultReader := MonadChainTo(failingReader, secondReader)
|
||||
result := resultReader(context.Background())
|
||||
result := resultReader(t.Context())
|
||||
|
||||
assert.Equal(t, E.Left[float64](testErr), result)
|
||||
assert.True(t, firstExecuted, "first reader should be executed")
|
||||
@@ -316,7 +317,7 @@ func TestMonadChainTo(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOrElse(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test OrElse with Right - should pass through unchanged
|
||||
t.Run("Right value unchanged", func(t *testing.T) {
|
||||
@@ -380,3 +381,613 @@ func TestOrElse(t *testing.T) {
|
||||
assert.Equal(t, E.Of[error](123), res)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromIO tests the FromIO function
|
||||
func TestFromIO(t *testing.T) {
|
||||
t.Run("lifts IO computation into ReaderResult", func(t *testing.T) {
|
||||
ioOp := func() int { return 42 }
|
||||
rr := FromIO(ioOp)
|
||||
result := rr(t.Context())
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
})
|
||||
|
||||
t.Run("executes IO side effects", func(t *testing.T) {
|
||||
executed := false
|
||||
ioOp := func() int {
|
||||
executed = true
|
||||
return 100
|
||||
}
|
||||
rr := FromIO(ioOp)
|
||||
result := rr(t.Context())
|
||||
assert.True(t, executed, "IO operation should be executed")
|
||||
assert.Equal(t, E.Of[error](100), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromIOResult tests the FromIOResult function
|
||||
func TestFromIOResult(t *testing.T) {
|
||||
t.Run("lifts IOResult into ReaderResult on success", func(t *testing.T) {
|
||||
ioResult := func() E.Either[error, int] {
|
||||
return E.Of[error](42)
|
||||
}
|
||||
rr := FromIOResult(ioResult)
|
||||
result := rr(t.Context())
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
})
|
||||
|
||||
t.Run("lifts IOResult into ReaderResult on error", func(t *testing.T) {
|
||||
testErr := errors.New("io error")
|
||||
ioResult := func() E.Either[error, int] {
|
||||
return E.Left[int](testErr)
|
||||
}
|
||||
rr := FromIOResult(ioResult)
|
||||
result := rr(t.Context())
|
||||
assert.Equal(t, E.Left[int](testErr), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromReader tests the FromReader function
|
||||
func TestFromReader(t *testing.T) {
|
||||
t.Run("lifts Reader into ReaderResult", func(t *testing.T) {
|
||||
reader := func(ctx context.Context) int {
|
||||
return 42
|
||||
}
|
||||
rr := FromReader(reader)
|
||||
result := rr(t.Context())
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
})
|
||||
|
||||
t.Run("Reader can access context", func(t *testing.T) {
|
||||
type ctxKey string
|
||||
ctx := context.WithValue(t.Context(), ctxKey("key"), "value")
|
||||
reader := func(ctx context.Context) string {
|
||||
return ctx.Value(ctxKey("key")).(string)
|
||||
}
|
||||
rr := FromReader(reader)
|
||||
result := rr(ctx)
|
||||
assert.Equal(t, E.Of[error]("value"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromEither tests the FromEither function
|
||||
func TestFromEither(t *testing.T) {
|
||||
t.Run("lifts Right Either into ReaderResult", func(t *testing.T) {
|
||||
either := E.Of[error](42)
|
||||
rr := FromEither(either)
|
||||
result := rr(t.Context())
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
})
|
||||
|
||||
t.Run("lifts Left Either into ReaderResult", func(t *testing.T) {
|
||||
testErr := errors.New("test error")
|
||||
either := E.Left[int](testErr)
|
||||
rr := FromEither(either)
|
||||
result := rr(t.Context())
|
||||
assert.Equal(t, E.Left[int](testErr), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLeftRight tests the Left and Right functions
|
||||
func TestLeftRight(t *testing.T) {
|
||||
t.Run("Left creates error ReaderResult", func(t *testing.T) {
|
||||
testErr := errors.New("test error")
|
||||
rr := Left[int](testErr)
|
||||
result := rr(t.Context())
|
||||
assert.Equal(t, E.Left[int](testErr), result)
|
||||
})
|
||||
|
||||
t.Run("Right creates success ReaderResult", func(t *testing.T) {
|
||||
rr := Right(42)
|
||||
result := rr(t.Context())
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadMapAndMap tests MonadMap and Map functions
|
||||
func TestMonadMapAndMap(t *testing.T) {
|
||||
t.Run("MonadMap transforms success value", func(t *testing.T) {
|
||||
rr := Of(42)
|
||||
mapped := MonadMap(rr, func(x int) string {
|
||||
return F.Pipe1(x, func(n int) string { return "value: " + F.Pipe1(n, func(i int) string { return string(rune(i + 48)) }) })
|
||||
})
|
||||
result := mapped(t.Context())
|
||||
assert.True(t, E.IsRight(result))
|
||||
})
|
||||
|
||||
t.Run("Map creates operator that transforms value", func(t *testing.T) {
|
||||
toString := Map(func(x int) string {
|
||||
return "value"
|
||||
})
|
||||
rr := Of(42)
|
||||
result := toString(rr)(t.Context())
|
||||
assert.True(t, E.IsRight(result))
|
||||
})
|
||||
|
||||
t.Run("Map preserves errors", func(t *testing.T) {
|
||||
testErr := errors.New("test error")
|
||||
toString := Map(func(x int) string {
|
||||
return "value"
|
||||
})
|
||||
rr := Left[int](testErr)
|
||||
result := toString(rr)(t.Context())
|
||||
assert.Equal(t, E.Left[string](testErr), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadChainAndChain tests MonadChain and Chain functions
|
||||
func TestMonadChainAndChain(t *testing.T) {
|
||||
t.Run("MonadChain sequences computations", func(t *testing.T) {
|
||||
rr := Of(42)
|
||||
chained := MonadChain(rr, func(x int) ReaderResult[string] {
|
||||
return Of("result")
|
||||
})
|
||||
result := chained(t.Context())
|
||||
assert.Equal(t, E.Of[error]("result"), result)
|
||||
})
|
||||
|
||||
t.Run("Chain creates operator that sequences computations", func(t *testing.T) {
|
||||
chainOp := Chain(func(x int) ReaderResult[string] {
|
||||
return Of("result")
|
||||
})
|
||||
rr := Of(42)
|
||||
result := chainOp(rr)(t.Context())
|
||||
assert.Equal(t, E.Of[error]("result"), result)
|
||||
})
|
||||
|
||||
t.Run("Chain short-circuits on error", func(t *testing.T) {
|
||||
executed := false
|
||||
testErr := errors.New("test error")
|
||||
chainOp := Chain(func(x int) ReaderResult[string] {
|
||||
executed = true
|
||||
return Of("result")
|
||||
})
|
||||
rr := Left[int](testErr)
|
||||
result := chainOp(rr)(t.Context())
|
||||
assert.False(t, executed, "Chain should not execute on error")
|
||||
assert.Equal(t, E.Left[string](testErr), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsk tests the Ask function
|
||||
func TestAsk(t *testing.T) {
|
||||
t.Run("Ask returns the context", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
rr := Ask()
|
||||
result := rr(ctx)
|
||||
assert.True(t, E.IsRight(result))
|
||||
})
|
||||
|
||||
t.Run("Ask can be used in chain to access context", func(t *testing.T) {
|
||||
type ctxKey string
|
||||
ctx := context.WithValue(t.Context(), ctxKey("key"), "value")
|
||||
pipeline := F.Pipe1(
|
||||
Ask(),
|
||||
Chain(func(c context.Context) ReaderResult[string] {
|
||||
val := c.Value(ctxKey("key"))
|
||||
if val != nil {
|
||||
return Of(val.(string))
|
||||
}
|
||||
return Left[string](errors.New("key not found"))
|
||||
}),
|
||||
)
|
||||
result := pipeline(ctx)
|
||||
assert.Equal(t, E.Of[error]("value"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadChainEitherK tests MonadChainEitherK and ChainEitherK
|
||||
func TestMonadChainEitherK(t *testing.T) {
|
||||
t.Run("MonadChainEitherK sequences with Either function", func(t *testing.T) {
|
||||
rr := Of(42)
|
||||
chained := MonadChainEitherK(rr, func(x int) E.Either[error, string] {
|
||||
if x > 0 {
|
||||
return E.Of[error]("positive")
|
||||
}
|
||||
return E.Left[string](errors.New("not positive"))
|
||||
})
|
||||
result := chained(t.Context())
|
||||
assert.Equal(t, E.Of[error]("positive"), result)
|
||||
})
|
||||
|
||||
t.Run("ChainEitherK creates operator", func(t *testing.T) {
|
||||
validate := ChainEitherK(func(x int) E.Either[error, int] {
|
||||
if x > 0 {
|
||||
return E.Of[error](x)
|
||||
}
|
||||
return E.Left[int](errors.New("must be positive"))
|
||||
})
|
||||
result := validate(Of(42))(t.Context())
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadFlap tests MonadFlap and Flap
|
||||
func TestMonadFlap(t *testing.T) {
|
||||
t.Run("MonadFlap applies value to function", func(t *testing.T) {
|
||||
fabRR := Of(func(x int) string {
|
||||
return "value"
|
||||
})
|
||||
result := MonadFlap(fabRR, 42)(t.Context())
|
||||
assert.True(t, E.IsRight(result))
|
||||
})
|
||||
|
||||
t.Run("Flap creates operator", func(t *testing.T) {
|
||||
applyTo42 := Flap[string](42)
|
||||
fabRR := Of(func(x int) string {
|
||||
return "value"
|
||||
})
|
||||
result := applyTo42(fabRR)(t.Context())
|
||||
assert.True(t, E.IsRight(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestRead functions
|
||||
func TestReadFunctions(t *testing.T) {
|
||||
t.Run("Read executes ReaderResult with context", func(t *testing.T) {
|
||||
rr := Of(42)
|
||||
ctx := t.Context()
|
||||
runWithCtx := Read[int](ctx)
|
||||
result := runWithCtx(rr)
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
})
|
||||
|
||||
t.Run("ReadEither executes with Result context on success", func(t *testing.T) {
|
||||
rr := Of(42)
|
||||
ctxResult := E.Of[error](t.Context())
|
||||
runWithCtxResult := ReadEither[int](ctxResult)
|
||||
result := runWithCtxResult(rr)
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
})
|
||||
|
||||
t.Run("ReadEither returns error when context Result is error", func(t *testing.T) {
|
||||
rr := Of(42)
|
||||
testErr := errors.New("context error")
|
||||
ctxResult := E.Left[context.Context](testErr)
|
||||
runWithCtxResult := ReadEither[int](ctxResult)
|
||||
result := runWithCtxResult(rr)
|
||||
assert.Equal(t, E.Left[int](testErr), result)
|
||||
})
|
||||
|
||||
t.Run("ReadResult is alias for ReadEither", func(t *testing.T) {
|
||||
rr := Of(42)
|
||||
ctxResult := E.Of[error](t.Context())
|
||||
runWithCtxResult := ReadResult[int](ctxResult)
|
||||
result := runWithCtxResult(rr)
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadChainFirst tests MonadChainFirst and ChainFirst
|
||||
func TestMonadChainFirst(t *testing.T) {
|
||||
t.Run("MonadChainFirst executes second computation but returns first value", func(t *testing.T) {
|
||||
secondExecuted := false
|
||||
rr := Of(42)
|
||||
withSideEffect := MonadChainFirst(rr, func(x int) ReaderResult[string] {
|
||||
return func(ctx context.Context) E.Either[error, string] {
|
||||
secondExecuted = true
|
||||
return E.Of[error]("logged")
|
||||
}
|
||||
})
|
||||
result := withSideEffect(t.Context())
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
assert.True(t, secondExecuted, "second computation should execute")
|
||||
})
|
||||
|
||||
t.Run("ChainFirst creates operator", func(t *testing.T) {
|
||||
secondExecuted := false
|
||||
logValue := ChainFirst(func(x int) ReaderResult[string] {
|
||||
return func(ctx context.Context) E.Either[error, string] {
|
||||
secondExecuted = true
|
||||
return E.Of[error]("logged")
|
||||
}
|
||||
})
|
||||
result := logValue(Of(42))(t.Context())
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
assert.True(t, secondExecuted)
|
||||
})
|
||||
}
|
||||
|
||||
// TestChainIOK tests ChainIOK and MonadChainIOK
|
||||
func TestChainIOK(t *testing.T) {
|
||||
t.Run("MonadChainIOK sequences with IO computation", func(t *testing.T) {
|
||||
ioExecuted := false
|
||||
rr := Of(42)
|
||||
withIO := MonadChainIOK(rr, func(x int) func() string {
|
||||
return func() string {
|
||||
ioExecuted = true
|
||||
return "done"
|
||||
}
|
||||
})
|
||||
result := withIO(t.Context())
|
||||
assert.Equal(t, E.Of[error]("done"), result)
|
||||
assert.True(t, ioExecuted)
|
||||
})
|
||||
|
||||
t.Run("ChainIOK creates operator", func(t *testing.T) {
|
||||
ioExecuted := false
|
||||
logIO := ChainIOK(func(x int) func() string {
|
||||
return func() string {
|
||||
ioExecuted = true
|
||||
return "logged"
|
||||
}
|
||||
})
|
||||
result := logIO(Of(42))(t.Context())
|
||||
assert.Equal(t, E.Of[error]("logged"), result)
|
||||
assert.True(t, ioExecuted)
|
||||
})
|
||||
}
|
||||
|
||||
// TestChainFirstIOK tests ChainFirstIOK, MonadChainFirstIOK, and TapIOK
|
||||
func TestChainFirstIOK(t *testing.T) {
|
||||
t.Run("MonadChainFirstIOK executes IO but returns original value", func(t *testing.T) {
|
||||
ioExecuted := false
|
||||
rr := Of(42)
|
||||
withLog := MonadChainFirstIOK(rr, func(x int) func() string {
|
||||
return func() string {
|
||||
ioExecuted = true
|
||||
return "logged"
|
||||
}
|
||||
})
|
||||
result := withLog(t.Context())
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
assert.True(t, ioExecuted)
|
||||
})
|
||||
|
||||
t.Run("ChainFirstIOK creates operator", func(t *testing.T) {
|
||||
ioExecuted := false
|
||||
logIO := ChainFirstIOK(func(x int) func() string {
|
||||
return func() string {
|
||||
ioExecuted = true
|
||||
return "logged"
|
||||
}
|
||||
})
|
||||
result := logIO(Of(42))(t.Context())
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
assert.True(t, ioExecuted)
|
||||
})
|
||||
|
||||
t.Run("TapIOK is alias for ChainFirstIOK", func(t *testing.T) {
|
||||
ioExecuted := false
|
||||
tapLog := TapIOK(func(x int) func() string {
|
||||
return func() string {
|
||||
ioExecuted = true
|
||||
return "logged"
|
||||
}
|
||||
})
|
||||
result := tapLog(Of(42))(t.Context())
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
assert.True(t, ioExecuted)
|
||||
})
|
||||
|
||||
t.Run("MonadTapIOK is alias for MonadChainFirstIOK", func(t *testing.T) {
|
||||
ioExecuted := false
|
||||
rr := Of(42)
|
||||
withLog := MonadTapIOK(rr, func(x int) func() string {
|
||||
return func() string {
|
||||
ioExecuted = true
|
||||
return "logged"
|
||||
}
|
||||
})
|
||||
result := withLog(t.Context())
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
assert.True(t, ioExecuted)
|
||||
})
|
||||
}
|
||||
|
||||
// TestChainIOEitherK tests ChainIOEitherK and ChainIOResultK
|
||||
func TestChainIOEitherK(t *testing.T) {
|
||||
t.Run("ChainIOEitherK sequences with IOResult on success", func(t *testing.T) {
|
||||
ioResultOp := ChainIOEitherK(func(x int) func() E.Either[error, string] {
|
||||
return func() E.Either[error, string] {
|
||||
if x > 0 {
|
||||
return E.Of[error]("positive")
|
||||
}
|
||||
return E.Left[string](errors.New("not positive"))
|
||||
}
|
||||
})
|
||||
result := ioResultOp(Of(42))(t.Context())
|
||||
assert.Equal(t, E.Of[error]("positive"), result)
|
||||
})
|
||||
|
||||
t.Run("ChainIOEitherK propagates IOResult error", func(t *testing.T) {
|
||||
testErr := errors.New("io error")
|
||||
ioResultOp := ChainIOEitherK(func(x int) func() E.Either[error, string] {
|
||||
return func() E.Either[error, string] {
|
||||
return E.Left[string](testErr)
|
||||
}
|
||||
})
|
||||
result := ioResultOp(Of(42))(t.Context())
|
||||
assert.Equal(t, E.Left[string](testErr), result)
|
||||
})
|
||||
|
||||
t.Run("ChainIOResultK is alias for ChainIOEitherK", func(t *testing.T) {
|
||||
ioResultOp := ChainIOResultK(func(x int) func() E.Either[error, string] {
|
||||
return func() E.Either[error, string] {
|
||||
return E.Of[error]("value")
|
||||
}
|
||||
})
|
||||
result := ioResultOp(Of(42))(t.Context())
|
||||
assert.Equal(t, E.Of[error]("value"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestReadIO tests ReadIO, ReadIOEither, and ReadIOResult
|
||||
func TestReadIO(t *testing.T) {
|
||||
t.Run("ReadIO executes with IO context", func(t *testing.T) {
|
||||
getCtx := func() context.Context { return t.Context() }
|
||||
rr := Of(42)
|
||||
runWithIO := ReadIO[int](getCtx)
|
||||
ioResult := runWithIO(rr)
|
||||
result := ioResult()
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
})
|
||||
|
||||
t.Run("ReadIOEither executes with IOResult context on success", func(t *testing.T) {
|
||||
getCtx := func() E.Either[error, context.Context] {
|
||||
return E.Of[error](t.Context())
|
||||
}
|
||||
rr := Of(42)
|
||||
runWithIOResult := ReadIOEither[int](getCtx)
|
||||
ioResult := runWithIOResult(rr)
|
||||
result := ioResult()
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
})
|
||||
|
||||
t.Run("ReadIOEither returns error when IOResult context is error", func(t *testing.T) {
|
||||
testErr := errors.New("context error")
|
||||
getCtx := func() E.Either[error, context.Context] {
|
||||
return E.Left[context.Context](testErr)
|
||||
}
|
||||
rr := Of(42)
|
||||
runWithIOResult := ReadIOEither[int](getCtx)
|
||||
ioResult := runWithIOResult(rr)
|
||||
result := ioResult()
|
||||
assert.Equal(t, E.Left[int](testErr), result)
|
||||
})
|
||||
|
||||
t.Run("ReadIOResult is alias for ReadIOEither", func(t *testing.T) {
|
||||
getCtx := func() E.Either[error, context.Context] {
|
||||
return E.Of[error](t.Context())
|
||||
}
|
||||
rr := Of(42)
|
||||
runWithIOResult := ReadIOResult[int](getCtx)
|
||||
ioResult := runWithIOResult(rr)
|
||||
result := ioResult()
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestChainFirstLeft tests ChainFirstLeft, ChainFirstLeftIOK, and TapLeftIOK
|
||||
func TestChainFirstLeft(t *testing.T) {
|
||||
t.Run("ChainFirstLeft executes on error but preserves it", func(t *testing.T) {
|
||||
errorHandled := false
|
||||
testErr := errors.New("test error")
|
||||
logError := ChainFirstLeft[int](func(err error) ReaderResult[string] {
|
||||
return func(ctx context.Context) E.Either[error, string] {
|
||||
errorHandled = true
|
||||
return E.Of[error]("logged")
|
||||
}
|
||||
})
|
||||
rr := Left[int](testErr)
|
||||
result := logError(rr)(t.Context())
|
||||
assert.Equal(t, E.Left[int](testErr), result)
|
||||
assert.True(t, errorHandled, "error handler should execute")
|
||||
})
|
||||
|
||||
t.Run("ChainFirstLeft does not execute on success", func(t *testing.T) {
|
||||
errorHandled := false
|
||||
logError := ChainFirstLeft[int](func(err error) ReaderResult[string] {
|
||||
return func(ctx context.Context) E.Either[error, string] {
|
||||
errorHandled = true
|
||||
return E.Of[error]("logged")
|
||||
}
|
||||
})
|
||||
rr := Of(42)
|
||||
result := logError(rr)(t.Context())
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
assert.False(t, errorHandled, "error handler should not execute on success")
|
||||
})
|
||||
|
||||
t.Run("ChainFirstLeftIOK executes IO on error", func(t *testing.T) {
|
||||
ioExecuted := false
|
||||
testErr := errors.New("test error")
|
||||
logErrorIO := ChainFirstLeftIOK[int](func(err error) func() string {
|
||||
return func() string {
|
||||
ioExecuted = true
|
||||
return "logged"
|
||||
}
|
||||
})
|
||||
rr := Left[int](testErr)
|
||||
result := logErrorIO(rr)(t.Context())
|
||||
assert.Equal(t, E.Left[int](testErr), result)
|
||||
assert.True(t, ioExecuted)
|
||||
})
|
||||
|
||||
t.Run("TapLeftIOK is alias for ChainFirstLeftIOK", func(t *testing.T) {
|
||||
ioExecuted := false
|
||||
testErr := errors.New("test error")
|
||||
tapErrorIO := TapLeftIOK[int](func(err error) func() string {
|
||||
return func() string {
|
||||
ioExecuted = true
|
||||
return "logged"
|
||||
}
|
||||
})
|
||||
rr := Left[int](testErr)
|
||||
result := tapErrorIO(rr)(t.Context())
|
||||
assert.Equal(t, E.Left[int](testErr), result)
|
||||
assert.True(t, ioExecuted)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromPredicate tests the FromPredicate function
|
||||
func TestFromPredicate(t *testing.T) {
|
||||
t.Run("FromPredicate returns Right when predicate is true", func(t *testing.T) {
|
||||
isPositive := FromPredicate(
|
||||
func(x int) bool { return x > 0 },
|
||||
func(x int) error { return errors.New("not positive") },
|
||||
)
|
||||
result := isPositive(42)(t.Context())
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
})
|
||||
|
||||
t.Run("FromPredicate returns Left when predicate is false", func(t *testing.T) {
|
||||
isPositive := FromPredicate(
|
||||
func(x int) bool { return x > 0 },
|
||||
func(x int) error { return errors.New("not positive") },
|
||||
)
|
||||
result := isPositive(-1)(t.Context())
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadAp tests MonadAp and Ap
|
||||
func TestMonadAp(t *testing.T) {
|
||||
t.Run("MonadAp applies function to value", func(t *testing.T) {
|
||||
fabRR := Of(func(x int) string {
|
||||
return "value"
|
||||
})
|
||||
faRR := Of(42)
|
||||
result := MonadAp(fabRR, faRR)(t.Context())
|
||||
assert.True(t, E.IsRight(result))
|
||||
})
|
||||
|
||||
t.Run("Ap creates function that applies", func(t *testing.T) {
|
||||
faRR := Of(42)
|
||||
applyTo42 := Ap[int, string](faRR)
|
||||
fabRR := Of(func(x int) string {
|
||||
return "value"
|
||||
})
|
||||
result := applyTo42(fabRR)(t.Context())
|
||||
assert.True(t, E.IsRight(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestChainOptionK tests the ChainOptionK function
|
||||
func TestChainOptionK(t *testing.T) {
|
||||
t.Run("ChainOptionK returns Right when Option is Some", func(t *testing.T) {
|
||||
chainOpt := ChainOptionK[int, string](func() error {
|
||||
return errors.New("value not found")
|
||||
})
|
||||
optKleisli := func(x int) option.Option[string] {
|
||||
if x > 0 {
|
||||
return option.Some("value")
|
||||
}
|
||||
return option.None[string]()
|
||||
}
|
||||
operator := chainOpt(optKleisli)
|
||||
result := operator(Of(42))(t.Context())
|
||||
assert.True(t, E.IsRight(result))
|
||||
})
|
||||
|
||||
t.Run("ChainOptionK returns Left when Option is None", func(t *testing.T) {
|
||||
chainOpt := ChainOptionK[int, string](func() error {
|
||||
return errors.New("value not found")
|
||||
})
|
||||
optKleisli := func(x int) option.Option[string] {
|
||||
return option.None[string]()
|
||||
}
|
||||
operator := chainOpt(optKleisli)
|
||||
result := operator(Of(42))(t.Context())
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ import (
|
||||
//
|
||||
// Example - Context cancellation:
|
||||
//
|
||||
// ctx, cancel := context.WithCancel(context.Background())
|
||||
// ctx, cancel := context.WithCancel(t.Context())
|
||||
// cancel() // Cancel immediately
|
||||
//
|
||||
// computation := TailRec(someStep)
|
||||
|
||||
@@ -45,7 +45,7 @@ func TestTailRecFactorial(t *testing.T) {
|
||||
}
|
||||
|
||||
factorial := TailRec(factorialStep)
|
||||
result := factorial(State{5, 1})(context.Background())
|
||||
result := factorial(State{5, 1})(t.Context())
|
||||
|
||||
assert.Equal(t, R.Of(120), result)
|
||||
}
|
||||
@@ -68,7 +68,7 @@ func TestTailRecFibonacci(t *testing.T) {
|
||||
}
|
||||
|
||||
fib := TailRec(fibStep)
|
||||
result := fib(State{10, 0, 1})(context.Background())
|
||||
result := fib(State{10, 0, 1})(t.Context())
|
||||
|
||||
assert.Equal(t, R.Of(89), result) // 10th Fibonacci number
|
||||
}
|
||||
@@ -85,7 +85,7 @@ func TestTailRecCountdown(t *testing.T) {
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(10)(context.Background())
|
||||
result := countdown(10)(t.Context())
|
||||
|
||||
assert.Equal(t, R.Of(0), result)
|
||||
}
|
||||
@@ -99,7 +99,7 @@ func TestTailRecImmediateTermination(t *testing.T) {
|
||||
}
|
||||
|
||||
immediate := TailRec(immediateStep)
|
||||
result := immediate(42)(context.Background())
|
||||
result := immediate(42)(t.Context())
|
||||
|
||||
assert.Equal(t, R.Of(84), result)
|
||||
}
|
||||
@@ -116,7 +116,7 @@ func TestTailRecStackSafety(t *testing.T) {
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(10000)(context.Background())
|
||||
result := countdown(10000)(t.Context())
|
||||
|
||||
assert.Equal(t, R.Of(0), result)
|
||||
}
|
||||
@@ -138,7 +138,7 @@ func TestTailRecSumList(t *testing.T) {
|
||||
}
|
||||
|
||||
sumList := TailRec(sumStep)
|
||||
result := sumList(State{[]int{1, 2, 3, 4, 5}, 0})(context.Background())
|
||||
result := sumList(State{[]int{1, 2, 3, 4, 5}, 0})(t.Context())
|
||||
|
||||
assert.Equal(t, R.Of(15), result)
|
||||
}
|
||||
@@ -158,7 +158,7 @@ func TestTailRecCollatzConjecture(t *testing.T) {
|
||||
}
|
||||
|
||||
collatz := TailRec(collatzStep)
|
||||
result := collatz(10)(context.Background())
|
||||
result := collatz(10)(t.Context())
|
||||
|
||||
assert.Equal(t, R.Of(1), result)
|
||||
}
|
||||
@@ -180,7 +180,7 @@ func TestTailRecGCD(t *testing.T) {
|
||||
}
|
||||
|
||||
gcd := TailRec(gcdStep)
|
||||
result := gcd(State{48, 18})(context.Background())
|
||||
result := gcd(State{48, 18})(t.Context())
|
||||
|
||||
assert.Equal(t, R.Of(6), result)
|
||||
}
|
||||
@@ -202,7 +202,7 @@ func TestTailRecErrorPropagation(t *testing.T) {
|
||||
}
|
||||
|
||||
computation := TailRec(errorStep)
|
||||
result := computation(10)(context.Background())
|
||||
result := computation(10)(t.Context())
|
||||
|
||||
assert.True(t, R.IsLeft(result))
|
||||
_, err := R.Unwrap(result)
|
||||
@@ -211,7 +211,7 @@ func TestTailRecErrorPropagation(t *testing.T) {
|
||||
|
||||
// TestTailRecContextCancellationImmediate tests short circuit when context is already canceled
|
||||
func TestTailRecContextCancellationImmediate(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel() // Cancel immediately before execution
|
||||
|
||||
stepExecuted := false
|
||||
@@ -237,7 +237,7 @@ func TestTailRecContextCancellationImmediate(t *testing.T) {
|
||||
|
||||
// TestTailRecContextCancellationDuringExecution tests short circuit when context is canceled during execution
|
||||
func TestTailRecContextCancellationDuringExecution(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
executionCount := 0
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
@@ -266,7 +266,7 @@ func TestTailRecContextCancellationDuringExecution(t *testing.T) {
|
||||
|
||||
// TestTailRecContextWithTimeout tests behavior with timeout context
|
||||
func TestTailRecContextWithTimeout(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
executionCount := 0
|
||||
@@ -295,7 +295,7 @@ func TestTailRecContextWithTimeout(t *testing.T) {
|
||||
// TestTailRecContextWithCause tests that context.Cause is properly returned
|
||||
func TestTailRecContextWithCause(t *testing.T) {
|
||||
customErr := errors.New("custom cancellation reason")
|
||||
ctx, cancel := context.WithCancelCause(context.Background())
|
||||
ctx, cancel := context.WithCancelCause(t.Context())
|
||||
cancel(customErr)
|
||||
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
@@ -317,7 +317,7 @@ func TestTailRecContextWithCause(t *testing.T) {
|
||||
|
||||
// TestTailRecContextCancellationMultipleIterations tests that cancellation is checked on each iteration
|
||||
func TestTailRecContextCancellationMultipleIterations(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
executionCount := 0
|
||||
maxExecutions := 5
|
||||
@@ -348,7 +348,7 @@ func TestTailRecContextCancellationMultipleIterations(t *testing.T) {
|
||||
|
||||
// TestTailRecContextNotCanceled tests normal execution when context is not canceled
|
||||
func TestTailRecContextNotCanceled(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
executionCount := 0
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
@@ -386,7 +386,7 @@ func TestTailRecPowerOfTwo(t *testing.T) {
|
||||
}
|
||||
|
||||
power := TailRec(powerStep)
|
||||
result := power(State{0, 1, 10})(context.Background())
|
||||
result := power(State{0, 1, 10})(t.Context())
|
||||
|
||||
assert.Equal(t, R.Of(1024), result) // 2^10
|
||||
}
|
||||
@@ -412,7 +412,7 @@ func TestTailRecFindInRange(t *testing.T) {
|
||||
}
|
||||
|
||||
find := TailRec(findStep)
|
||||
result := find(State{0, 100, 42})(context.Background())
|
||||
result := find(State{0, 100, 42})(t.Context())
|
||||
|
||||
assert.Equal(t, R.Of(42), result)
|
||||
}
|
||||
@@ -438,7 +438,7 @@ func TestTailRecFindNotInRange(t *testing.T) {
|
||||
}
|
||||
|
||||
find := TailRec(findStep)
|
||||
result := find(State{0, 100, 200})(context.Background())
|
||||
result := find(State{0, 100, 200})(t.Context())
|
||||
|
||||
assert.Equal(t, R.Of(-1), result)
|
||||
}
|
||||
@@ -448,7 +448,7 @@ func TestTailRecWithContextValue(t *testing.T) {
|
||||
type contextKey string
|
||||
const multiplierKey contextKey = "multiplier"
|
||||
|
||||
ctx := context.WithValue(context.Background(), multiplierKey, 3)
|
||||
ctx := context.WithValue(t.Context(), multiplierKey, 3)
|
||||
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
|
||||
@@ -492,7 +492,7 @@ func TestTailRecComplexState(t *testing.T) {
|
||||
}
|
||||
|
||||
computation := TailRec(complexStep)
|
||||
result := computation(ComplexState{5, 0, 1, false})(context.Background())
|
||||
result := computation(ComplexState{5, 0, 1, false})(t.Context())
|
||||
|
||||
assert.Equal(t, R.Of("sum=15, product=120"), result)
|
||||
}
|
||||
|
||||
@@ -90,7 +90,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()
|
||||
// finalResult := retryingFetch(ctx)
|
||||
//
|
||||
|
||||
@@ -20,20 +20,201 @@ import (
|
||||
"github.com/IBM/fp-go/v2/tuple"
|
||||
)
|
||||
|
||||
// SequenceT converts n inputs of higher kinded types into a higher kinded types of n strongly typed values, represented as a tuple
|
||||
// SequenceT functions convert multiple ReaderResult values into a single ReaderResult containing a tuple.
|
||||
// These functions execute all input ReaderResults with the same context and combine their results.
|
||||
//
|
||||
// IMPORTANT: All ReaderResults are executed, even if one fails. The implementation uses applicative
|
||||
// semantics, which means all computations run to collect their results. If any ReaderResult fails
|
||||
// (returns Left), the entire sequence fails and returns the first error encountered, but all
|
||||
// ReaderResults will have been executed for their side effects.
|
||||
//
|
||||
// These functions are useful for:
|
||||
// - Combining multiple independent computations that all need the same context
|
||||
// - Collecting results from operations where all side effects should occur
|
||||
// - Building complex data structures from multiple ReaderResult sources
|
||||
// - Validating multiple fields where you want all validations to run
|
||||
//
|
||||
// The sequence executes in order (left to right), so side effects occur in that order.
|
||||
|
||||
// SequenceT1 converts a single ReaderResult into a ReaderResult containing a 1-tuple.
|
||||
// This is primarily useful for consistency in generic code or when you need to wrap
|
||||
// a single value in a tuple structure.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the value in the ReaderResult
|
||||
//
|
||||
// Parameters:
|
||||
// - a: The ReaderResult to wrap in a tuple
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderResult containing a Tuple1 with the value from the input
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rr := readerresult.Of(42)
|
||||
// tupled := readerresult.SequenceT1(rr)
|
||||
// result := tupled(t.Context())
|
||||
// // result is Right(Tuple1{F1: 42})
|
||||
//
|
||||
//go:inline
|
||||
func SequenceT1[A any](a ReaderResult[A]) ReaderResult[tuple.Tuple1[A]] {
|
||||
return readereither.SequenceT1(a)
|
||||
}
|
||||
|
||||
// SequenceT2 combines two ReaderResults into a single ReaderResult containing a 2-tuple.
|
||||
// Both ReaderResults are executed with the same context. If either fails, the entire
|
||||
// sequence fails.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the first value
|
||||
// - B: The type of the second value
|
||||
//
|
||||
// Parameters:
|
||||
// - a: The first ReaderResult
|
||||
// - b: The second ReaderResult
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderResult containing a Tuple2 with both values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getName := readerresult.Of("Alice")
|
||||
// getAge := readerresult.Of(30)
|
||||
// combined := readerresult.SequenceT2(getName, getAge)
|
||||
// result := combined(t.Context())
|
||||
// // result is Right(Tuple2{F1: "Alice", F2: 30})
|
||||
//
|
||||
// Example with error:
|
||||
//
|
||||
// getName := readerresult.Of("Alice")
|
||||
// getAge := readerresult.Left[int](errors.New("age not found"))
|
||||
// combined := readerresult.SequenceT2(getName, getAge)
|
||||
// result := combined(t.Context())
|
||||
// // result is Left(error("age not found"))
|
||||
//
|
||||
//go:inline
|
||||
func SequenceT2[A, B any](a ReaderResult[A], b ReaderResult[B]) ReaderResult[tuple.Tuple2[A, B]] {
|
||||
return readereither.SequenceT2(a, b)
|
||||
}
|
||||
|
||||
// SequenceT3 combines three ReaderResults into a single ReaderResult containing a 3-tuple.
|
||||
// All ReaderResults are executed sequentially with the same context. If any fails,
|
||||
// the entire sequence fails immediately.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the first value
|
||||
// - B: The type of the second value
|
||||
// - C: The type of the third value
|
||||
//
|
||||
// Parameters:
|
||||
// - a: The first ReaderResult
|
||||
// - b: The second ReaderResult
|
||||
// - c: The third ReaderResult
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderResult containing a Tuple3 with all three values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getUserID := readerresult.Of(123)
|
||||
// getUserName := readerresult.Of("Alice")
|
||||
// getUserEmail := readerresult.Of("alice@example.com")
|
||||
// combined := readerresult.SequenceT3(getUserID, getUserName, getUserEmail)
|
||||
// result := combined(t.Context())
|
||||
// // result is Right(Tuple3{F1: 123, F2: "Alice", F3: "alice@example.com"})
|
||||
//
|
||||
// Example with context-aware operations:
|
||||
//
|
||||
// fetchUser := func(ctx context.Context) result.Result[string] {
|
||||
// if ctx.Err() != nil {
|
||||
// return result.Error[string](ctx.Err())
|
||||
// }
|
||||
// return result.Of("Alice")
|
||||
// }
|
||||
// fetchAge := func(ctx context.Context) result.Result[int] {
|
||||
// return result.Of(30)
|
||||
// }
|
||||
// fetchCity := func(ctx context.Context) result.Result[string] {
|
||||
// return result.Of("NYC")
|
||||
// }
|
||||
// combined := readerresult.SequenceT3(fetchUser, fetchAge, fetchCity)
|
||||
// result := combined(t.Context())
|
||||
// // result is Right(Tuple3{F1: "Alice", F2: 30, F3: "NYC"})
|
||||
//
|
||||
//go:inline
|
||||
func SequenceT3[A, B, C any](a ReaderResult[A], b ReaderResult[B], c ReaderResult[C]) ReaderResult[tuple.Tuple3[A, B, C]] {
|
||||
return readereither.SequenceT3(a, b, c)
|
||||
}
|
||||
|
||||
// SequenceT4 combines four ReaderResults into a single ReaderResult containing a 4-tuple.
|
||||
// All ReaderResults are executed sequentially with the same context. If any fails,
|
||||
// the entire sequence fails immediately without executing the remaining ones.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the first value
|
||||
// - B: The type of the second value
|
||||
// - C: The type of the third value
|
||||
// - D: The type of the fourth value
|
||||
//
|
||||
// Parameters:
|
||||
// - a: The first ReaderResult
|
||||
// - b: The second ReaderResult
|
||||
// - c: The third ReaderResult
|
||||
// - d: The fourth ReaderResult
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderResult containing a Tuple4 with all four values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getID := readerresult.Of(123)
|
||||
// getName := readerresult.Of("Alice")
|
||||
// getEmail := readerresult.Of("alice@example.com")
|
||||
// getAge := readerresult.Of(30)
|
||||
// combined := readerresult.SequenceT4(getID, getName, getEmail, getAge)
|
||||
// result := combined(t.Context())
|
||||
// // result is Right(Tuple4{F1: 123, F2: "Alice", F3: "alice@example.com", F4: 30})
|
||||
//
|
||||
// Example with early failure:
|
||||
//
|
||||
// getID := readerresult.Of(123)
|
||||
// getName := readerresult.Left[string](errors.New("name not found"))
|
||||
// getEmail := readerresult.Of("alice@example.com") // Not executed
|
||||
// getAge := readerresult.Of(30) // Not executed
|
||||
// combined := readerresult.SequenceT4(getID, getName, getEmail, getAge)
|
||||
// result := combined(t.Context())
|
||||
// // result is Left(error("name not found"))
|
||||
// // getEmail and getAge are never executed due to early failure
|
||||
//
|
||||
// Example building a complex structure:
|
||||
//
|
||||
// type UserProfile struct {
|
||||
// ID int
|
||||
// Name string
|
||||
// Email string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// fetchUserData := readerresult.SequenceT4(
|
||||
// fetchUserID(userID),
|
||||
// fetchUserName(userID),
|
||||
// fetchUserEmail(userID),
|
||||
// fetchUserAge(userID),
|
||||
// )
|
||||
//
|
||||
// buildProfile := readerresult.Map(func(t tuple.Tuple4[int, string, string, int]) UserProfile {
|
||||
// return UserProfile{
|
||||
// ID: t.F1,
|
||||
// Name: t.F2,
|
||||
// Email: t.F3,
|
||||
// Age: t.F4,
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// userProfile := F.Pipe1(fetchUserData, buildProfile)
|
||||
// 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]] {
|
||||
return readereither.SequenceT4(a, b, c, d)
|
||||
}
|
||||
|
||||
460
v2/context/readerresult/sequence_test.go
Normal file
460
v2/context/readerresult/sequence_test.go
Normal file
@@ -0,0 +1,460 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestSequenceT1 tests the SequenceT1 function
|
||||
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(t.Context())
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
val, _ := E.Unwrap(result)
|
||||
assert.Equal(t, 42, val.F1)
|
||||
})
|
||||
|
||||
t.Run("preserves error", func(t *testing.T) {
|
||||
testErr := errors.New("test error")
|
||||
rr := Left[int](testErr)
|
||||
tupled := SequenceT1(rr)
|
||||
result := tupled(t.Context())
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, err := E.UnwrapError(result)
|
||||
assert.Equal(t, testErr, err)
|
||||
})
|
||||
|
||||
t.Run("respects context cancellation", func(t *testing.T) {
|
||||
rr := func(ctx context.Context) E.Either[error, int] {
|
||||
if ctx.Err() != nil {
|
||||
return E.Left[int](ctx.Err())
|
||||
}
|
||||
return E.Of[error](42)
|
||||
}
|
||||
|
||||
tupled := SequenceT1(rr)
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
result := tupled(ctx)
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestSequenceT2 tests the SequenceT2 function
|
||||
func TestSequenceT2(t *testing.T) {
|
||||
t.Run("combines two success values into tuple", func(t *testing.T) {
|
||||
getName := Of("Alice")
|
||||
getAge := Of(30)
|
||||
|
||||
combined := SequenceT2(getName, getAge)
|
||||
result := combined(t.Context())
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
val, _ := E.Unwrap(result)
|
||||
assert.Equal(t, "Alice", val.F1)
|
||||
assert.Equal(t, 30, val.F2)
|
||||
})
|
||||
|
||||
t.Run("fails if first ReaderResult fails", func(t *testing.T) {
|
||||
testErr := errors.New("name not found")
|
||||
getName := Left[string](testErr)
|
||||
getAge := Of(30)
|
||||
|
||||
combined := SequenceT2(getName, getAge)
|
||||
result := combined(t.Context())
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, err := E.UnwrapError(result)
|
||||
assert.Equal(t, testErr, err)
|
||||
})
|
||||
|
||||
t.Run("fails if second ReaderResult fails", func(t *testing.T) {
|
||||
testErr := errors.New("age not found")
|
||||
getName := Of("Alice")
|
||||
getAge := Left[int](testErr)
|
||||
|
||||
combined := SequenceT2(getName, getAge)
|
||||
result := combined(t.Context())
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, err := E.UnwrapError(result)
|
||||
assert.Equal(t, testErr, err)
|
||||
})
|
||||
|
||||
t.Run("executes both ReaderResults with same context", func(t *testing.T) {
|
||||
type ctxKey string
|
||||
ctx := context.WithValue(t.Context(), ctxKey("key"), "shared")
|
||||
|
||||
getName := func(ctx context.Context) E.Either[error, string] {
|
||||
val := ctx.Value(ctxKey("key"))
|
||||
if val != nil {
|
||||
return E.Of[error](val.(string))
|
||||
}
|
||||
return E.Left[string](errors.New("key not found"))
|
||||
}
|
||||
|
||||
getAge := func(ctx context.Context) E.Either[error, int] {
|
||||
val := ctx.Value(ctxKey("key"))
|
||||
if val != nil {
|
||||
return E.Of[error](len(val.(string)))
|
||||
}
|
||||
return E.Left[int](errors.New("key not found"))
|
||||
}
|
||||
|
||||
combined := SequenceT2(getName, getAge)
|
||||
result := combined(ctx)
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
val, _ := E.Unwrap(result)
|
||||
assert.Equal(t, "shared", val.F1)
|
||||
assert.Equal(t, 6, val.F2) // len("shared")
|
||||
})
|
||||
|
||||
t.Run("executes all ReaderResults even if one fails", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
secondExecuted := false
|
||||
|
||||
first := func(ctx context.Context) E.Either[error, int] {
|
||||
firstExecuted = true
|
||||
return E.Left[int](errors.New("first failed"))
|
||||
}
|
||||
|
||||
second := func(ctx context.Context) E.Either[error, string] {
|
||||
secondExecuted = true
|
||||
return E.Of[error]("second")
|
||||
}
|
||||
|
||||
combined := SequenceT2(first, second)
|
||||
result := combined(t.Context())
|
||||
|
||||
assert.True(t, firstExecuted, "first should be executed")
|
||||
assert.True(t, secondExecuted, "second should be executed (applicative semantics)")
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestSequenceT3 tests the SequenceT3 function
|
||||
func TestSequenceT3(t *testing.T) {
|
||||
t.Run("combines three success values into tuple", func(t *testing.T) {
|
||||
getUserID := Of(123)
|
||||
getUserName := Of("Alice")
|
||||
getUserEmail := Of("alice@example.com")
|
||||
|
||||
combined := SequenceT3(getUserID, getUserName, getUserEmail)
|
||||
result := combined(t.Context())
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
val, _ := E.Unwrap(result)
|
||||
assert.Equal(t, 123, val.F1)
|
||||
assert.Equal(t, "Alice", val.F2)
|
||||
assert.Equal(t, "alice@example.com", val.F3)
|
||||
})
|
||||
|
||||
t.Run("fails if any ReaderResult fails", func(t *testing.T) {
|
||||
testErr := errors.New("email not found")
|
||||
getUserID := Of(123)
|
||||
getUserName := Of("Alice")
|
||||
getUserEmail := Left[string](testErr)
|
||||
|
||||
combined := SequenceT3(getUserID, getUserName, getUserEmail)
|
||||
result := combined(t.Context())
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, err := E.UnwrapError(result)
|
||||
assert.Equal(t, testErr, err)
|
||||
})
|
||||
|
||||
t.Run("executes all ReaderResults even if one fails", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
secondExecuted := false
|
||||
thirdExecuted := false
|
||||
|
||||
first := func(ctx context.Context) E.Either[error, int] {
|
||||
firstExecuted = true
|
||||
return E.Of[error](1)
|
||||
}
|
||||
|
||||
second := func(ctx context.Context) E.Either[error, int] {
|
||||
secondExecuted = true
|
||||
return E.Left[int](errors.New("second failed"))
|
||||
}
|
||||
|
||||
third := func(ctx context.Context) E.Either[error, int] {
|
||||
thirdExecuted = true
|
||||
return E.Of[error](3)
|
||||
}
|
||||
|
||||
combined := SequenceT3(first, second, third)
|
||||
result := combined(t.Context())
|
||||
|
||||
assert.True(t, firstExecuted, "first should be executed")
|
||||
assert.True(t, secondExecuted, "second should be executed")
|
||||
assert.True(t, thirdExecuted, "third should be executed (applicative semantics)")
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("respects context cancellation", func(t *testing.T) {
|
||||
getUserID := func(ctx context.Context) E.Either[error, int] {
|
||||
if ctx.Err() != nil {
|
||||
return E.Left[int](ctx.Err())
|
||||
}
|
||||
return E.Of[error](123)
|
||||
}
|
||||
|
||||
getUserName := Of("Alice")
|
||||
getUserEmail := Of("alice@example.com")
|
||||
|
||||
combined := SequenceT3(getUserID, getUserName, getUserEmail)
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
result := combined(ctx)
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestSequenceT4 tests the SequenceT4 function
|
||||
func TestSequenceT4(t *testing.T) {
|
||||
t.Run("combines four success values into tuple", func(t *testing.T) {
|
||||
getID := Of(123)
|
||||
getName := Of("Alice")
|
||||
getEmail := Of("alice@example.com")
|
||||
getAge := Of(30)
|
||||
|
||||
combined := SequenceT4(getID, getName, getEmail, getAge)
|
||||
result := combined(t.Context())
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
val, _ := E.Unwrap(result)
|
||||
assert.Equal(t, 123, val.F1)
|
||||
assert.Equal(t, "Alice", val.F2)
|
||||
assert.Equal(t, "alice@example.com", val.F3)
|
||||
assert.Equal(t, 30, val.F4)
|
||||
})
|
||||
|
||||
t.Run("fails if any ReaderResult fails", func(t *testing.T) {
|
||||
testErr := errors.New("name not found")
|
||||
getID := Of(123)
|
||||
getName := Left[string](testErr)
|
||||
getEmail := Of("alice@example.com")
|
||||
getAge := Of(30)
|
||||
|
||||
combined := SequenceT4(getID, getName, getEmail, getAge)
|
||||
result := combined(t.Context())
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, err := E.UnwrapError(result)
|
||||
assert.Equal(t, testErr, err)
|
||||
})
|
||||
|
||||
t.Run("executes all ReaderResults even if one fails", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
secondExecuted := false
|
||||
thirdExecuted := false
|
||||
fourthExecuted := false
|
||||
|
||||
first := func(ctx context.Context) E.Either[error, int] {
|
||||
firstExecuted = true
|
||||
return E.Of[error](1)
|
||||
}
|
||||
|
||||
second := func(ctx context.Context) E.Either[error, int] {
|
||||
secondExecuted = true
|
||||
return E.Left[int](errors.New("second failed"))
|
||||
}
|
||||
|
||||
third := func(ctx context.Context) E.Either[error, int] {
|
||||
thirdExecuted = true
|
||||
return E.Of[error](3)
|
||||
}
|
||||
|
||||
fourth := func(ctx context.Context) E.Either[error, int] {
|
||||
fourthExecuted = true
|
||||
return E.Of[error](4)
|
||||
}
|
||||
|
||||
combined := SequenceT4(first, second, third, fourth)
|
||||
result := combined(t.Context())
|
||||
|
||||
assert.True(t, firstExecuted, "first should be executed")
|
||||
assert.True(t, secondExecuted, "second should be executed")
|
||||
assert.True(t, thirdExecuted, "third should be executed (applicative semantics)")
|
||||
assert.True(t, fourthExecuted, "fourth should be executed (applicative semantics)")
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("can be used to build complex structures", func(t *testing.T) {
|
||||
type UserProfile struct {
|
||||
ID int
|
||||
Name string
|
||||
Email string
|
||||
Age int
|
||||
}
|
||||
|
||||
fetchUserData := SequenceT4(
|
||||
Of(123),
|
||||
Of("Alice"),
|
||||
Of("alice@example.com"),
|
||||
Of(30),
|
||||
)
|
||||
|
||||
buildProfile := Map(func(t tuple.Tuple4[int, string, string, int]) UserProfile {
|
||||
return UserProfile{
|
||||
ID: t.F1,
|
||||
Name: t.F2,
|
||||
Email: t.F3,
|
||||
Age: t.F4,
|
||||
}
|
||||
})
|
||||
|
||||
userProfile := func(ctx context.Context) E.Either[error, UserProfile] {
|
||||
tupleResult := fetchUserData(ctx)
|
||||
if E.IsLeft(tupleResult) {
|
||||
_, err := E.UnwrapError(tupleResult)
|
||||
return E.Left[UserProfile](err)
|
||||
}
|
||||
tupleVal, _ := E.Unwrap(tupleResult)
|
||||
return buildProfile(Of(tupleVal))(ctx)
|
||||
}
|
||||
|
||||
result := userProfile(t.Context())
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
profile, _ := E.Unwrap(result)
|
||||
assert.Equal(t, 123, profile.ID)
|
||||
assert.Equal(t, "Alice", profile.Name)
|
||||
assert.Equal(t, "alice@example.com", profile.Email)
|
||||
assert.Equal(t, 30, profile.Age)
|
||||
})
|
||||
|
||||
t.Run("executes all with same context", func(t *testing.T) {
|
||||
type ctxKey string
|
||||
ctx := context.WithValue(t.Context(), ctxKey("multiplier"), 2)
|
||||
|
||||
getBase := func(ctx context.Context) E.Either[error, int] {
|
||||
return E.Of[error](10)
|
||||
}
|
||||
|
||||
multiply := func(ctx context.Context) E.Either[error, int] {
|
||||
mult := ctx.Value(ctxKey("multiplier")).(int)
|
||||
return E.Of[error](mult)
|
||||
}
|
||||
|
||||
getResult := func(ctx context.Context) E.Either[error, int] {
|
||||
mult := ctx.Value(ctxKey("multiplier")).(int)
|
||||
return E.Of[error](10 * mult)
|
||||
}
|
||||
|
||||
getDescription := func(ctx context.Context) E.Either[error, string] {
|
||||
return E.Of[error]("calculated")
|
||||
}
|
||||
|
||||
combined := SequenceT4(getBase, multiply, getResult, getDescription)
|
||||
result := combined(ctx)
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
val, _ := E.Unwrap(result)
|
||||
assert.Equal(t, 10, val.F1)
|
||||
assert.Equal(t, 2, val.F2)
|
||||
assert.Equal(t, 20, val.F3)
|
||||
assert.Equal(t, "calculated", val.F4)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSequenceIntegration tests integration scenarios
|
||||
func TestSequenceIntegration(t *testing.T) {
|
||||
t.Run("SequenceT2 with Map to transform tuple", func(t *testing.T) {
|
||||
getName := Of("Alice")
|
||||
getAge := Of(30)
|
||||
|
||||
combined := SequenceT2(getName, getAge)
|
||||
formatted := Map(func(t tuple.Tuple2[string, int]) string {
|
||||
return t.F1 + " is " + string(rune(t.F2+48)) + " years old"
|
||||
})
|
||||
|
||||
pipeline := func(ctx context.Context) E.Either[error, string] {
|
||||
tupleResult := combined(ctx)
|
||||
if E.IsLeft(tupleResult) {
|
||||
_, err := E.UnwrapError(tupleResult)
|
||||
return E.Left[string](err)
|
||||
}
|
||||
tupleVal, _ := E.Unwrap(tupleResult)
|
||||
return formatted(Of(tupleVal))(ctx)
|
||||
}
|
||||
|
||||
result := pipeline(t.Context())
|
||||
assert.True(t, E.IsRight(result))
|
||||
})
|
||||
|
||||
t.Run("SequenceT3 with Chain for dependent operations", func(t *testing.T) {
|
||||
getX := Of(10)
|
||||
getY := Of(20)
|
||||
getZ := Of(30)
|
||||
|
||||
combined := SequenceT3(getX, getY, getZ)
|
||||
|
||||
sumTuple := func(t tuple.Tuple3[int, int, int]) ReaderResult[int] {
|
||||
return Of(t.F1 + t.F2 + t.F3)
|
||||
}
|
||||
|
||||
pipeline := func(ctx context.Context) E.Either[error, int] {
|
||||
tupleResult := combined(ctx)
|
||||
if E.IsLeft(tupleResult) {
|
||||
_, err := E.UnwrapError(tupleResult)
|
||||
return E.Left[int](err)
|
||||
}
|
||||
tupleVal, _ := E.Unwrap(tupleResult)
|
||||
return sumTuple(tupleVal)(ctx)
|
||||
}
|
||||
|
||||
result := pipeline(t.Context())
|
||||
assert.True(t, E.IsRight(result))
|
||||
val, _ := E.Unwrap(result)
|
||||
assert.Equal(t, 60, val) // 10 + 20 + 30
|
||||
})
|
||||
|
||||
t.Run("nested sequences", func(t *testing.T) {
|
||||
// Create two pairs
|
||||
pair1 := SequenceT2(Of(1), Of(2))
|
||||
pair2 := SequenceT2(Of(3), Of(4))
|
||||
|
||||
// Combine the pairs
|
||||
combined := SequenceT2(pair1, pair2)
|
||||
|
||||
result := combined(t.Context())
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
val, _ := E.Unwrap(result)
|
||||
assert.Equal(t, 1, val.F1.F1)
|
||||
assert.Equal(t, 2, val.F1.F2)
|
||||
assert.Equal(t, 3, val.F2.F1)
|
||||
assert.Equal(t, 4, val.F2.F2)
|
||||
})
|
||||
}
|
||||
@@ -15,6 +15,38 @@
|
||||
|
||||
// Package readerresult implements a specialization of the Reader monad assuming a golang context as the context of the monad and a standard golang error.
|
||||
//
|
||||
// # Side Effects and Context
|
||||
//
|
||||
// IMPORTANT: In contrast to the functional readerresult package (readerresult.ReaderResult[R, A]),
|
||||
// this context/readerresult package has side effects by design because it depends on context.Context,
|
||||
// which is inherently effectful:
|
||||
// - context.Context can be cancelled (ctx.Done() channel)
|
||||
// - context.Context has deadlines and timeouts (ctx.Deadline())
|
||||
// - context.Context carries request-scoped values (ctx.Value())
|
||||
// - context.Context propagates cancellation signals across goroutines
|
||||
//
|
||||
// This means that ReaderResult[A] = func(context.Context) (A, error) represents an EFFECTFUL computation,
|
||||
// not a pure function. The computation's behavior can change based on the context's state (cancelled,
|
||||
// timed out, etc.), making it fundamentally different from a pure Reader monad.
|
||||
//
|
||||
// Comparison of packages:
|
||||
// - readerresult.ReaderResult[R, A] = func(R) Result[A] - PURE (R can be any type, no side effects)
|
||||
// - idiomatic/readerresult.ReaderResult[R, A] = func(R) (A, error) - EFFECTFUL (also uses context.Context)
|
||||
// - context/readerresult.ReaderResult[A] = func(context.Context) (A, error) - EFFECTFUL (uses context.Context)
|
||||
//
|
||||
// Use this package (context/readerresult) when you need:
|
||||
// - Cancellation support for long-running operations
|
||||
// - Timeout/deadline handling
|
||||
// - Request-scoped values (tracing IDs, user context, etc.)
|
||||
// - Integration with Go's standard context-aware APIs
|
||||
// - Idiomatic Go error handling with (value, error) tuples
|
||||
//
|
||||
// Use the functional readerresult package when you need:
|
||||
// - Pure dependency injection without side effects
|
||||
// - Testable computations with simple state/config objects
|
||||
// - Functional composition without context propagation
|
||||
// - Generic environment types (not limited to context.Context)
|
||||
//
|
||||
// # Pure vs Effectful Functions
|
||||
//
|
||||
// This package distinguishes between pure (side-effect free) and effectful (side-effectful) functions:
|
||||
@@ -45,6 +77,8 @@ import (
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
@@ -56,18 +90,245 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
Option[A any] = option.Option[A]
|
||||
Either[A any] = either.Either[error, A]
|
||||
Result[A any] = result.Result[A]
|
||||
// Option represents an optional value that may or may not be present.
|
||||
// This is an alias for option.Option[A].
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the value that may be present
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// opt := option.Some(42) // Option[int] with value
|
||||
// none := option.None[int]() // Option[int] without value
|
||||
Option[A any] = option.Option[A]
|
||||
|
||||
// Either represents a value that can be either a Left (error) or Right (success).
|
||||
// This is specialized to use error as the Left type.
|
||||
// This is an alias for either.Either[error, A].
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the Right (success) value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// success := either.Right[error, int](42) // Right(42)
|
||||
// failure := either.Left[int](errors.New("failed")) // Left(error)
|
||||
Either[A any] = either.Either[error, A]
|
||||
|
||||
// Result represents a computation that can either succeed with a value or fail with an error.
|
||||
// This is an alias for result.Result[A], which is equivalent to Either[error, A].
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the success value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// success := result.Of[error](42) // Right(42)
|
||||
// failure := result.Error[int](errors.New("failed")) // Left(error)
|
||||
Result[A any] = result.Result[A]
|
||||
|
||||
// Reader represents a computation that depends on an environment R to produce a value A.
|
||||
// This is an alias for reader.Reader[R, A].
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The type of the environment/context
|
||||
// - A: The type of the produced value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Port int }
|
||||
// getPort := func(cfg Config) int { return cfg.Port }
|
||||
// // getPort is a Reader[Config, int]
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
// ReaderResult is a specialization of the Reader monad for the typical golang scenario
|
||||
|
||||
// ReaderResult is a specialization of the Reader monad for the typical Go scenario.
|
||||
// It represents an effectful computation that:
|
||||
// - Depends on context.Context (for cancellation, deadlines, values)
|
||||
// - Can fail with an error
|
||||
// - Produces a value of type A on success
|
||||
//
|
||||
// IMPORTANT: This is an EFFECTFUL type because context.Context is effectful.
|
||||
// The computation's behavior can change based on context state (cancelled, timed out, etc.).
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the success value
|
||||
//
|
||||
// Signature:
|
||||
//
|
||||
// type ReaderResult[A any] = func(context.Context) Result[A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getUserByID := func(ctx context.Context) result.Result[User] {
|
||||
// if ctx.Err() != nil {
|
||||
// return result.Error[User](ctx.Err())
|
||||
// }
|
||||
// // Fetch user from database
|
||||
// return result.Of(User{ID: 123, Name: "Alice"})
|
||||
// }
|
||||
// // getUserByID is a ReaderResult[User]
|
||||
ReaderResult[A any] = readereither.ReaderEither[context.Context, error, A]
|
||||
|
||||
Kleisli[A, B any] = reader.Reader[A, ReaderResult[B]]
|
||||
Operator[A, B any] = Kleisli[ReaderResult[A], B]
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
Prism[S, T any] = prism.Prism[S, T]
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
// Kleisli represents a function that takes a value of type A and returns a ReaderResult[B].
|
||||
// This is the fundamental building block for composing ReaderResult computations.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The input type
|
||||
// - B: The output type (wrapped in ReaderResult)
|
||||
//
|
||||
// Signature:
|
||||
//
|
||||
// type Kleisli[A, B any] = func(A) ReaderResult[B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getUserByID := func(id int) readerresult.ReaderResult[User] {
|
||||
// return func(ctx context.Context) result.Result[User] {
|
||||
// // Fetch user from database
|
||||
// return result.Of(User{ID: id, Name: "Alice"})
|
||||
// }
|
||||
// }
|
||||
// // getUserByID is a Kleisli[int, User]
|
||||
Kleisli[A, B any] = reader.Reader[A, ReaderResult[B]]
|
||||
|
||||
// Operator represents a function that transforms one ReaderResult into another.
|
||||
// This is a specialized Kleisli where the input is itself a ReaderResult.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The input ReaderResult's success type
|
||||
// - B: The output ReaderResult's success type
|
||||
//
|
||||
// Signature:
|
||||
//
|
||||
// type Operator[A, B any] = func(ReaderResult[A]) ReaderResult[B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// mapToString := readerresult.Map(func(x int) string {
|
||||
// return fmt.Sprintf("value: %d", x)
|
||||
// })
|
||||
// // mapToString is an Operator[int, string]
|
||||
Operator[A, B any] = Kleisli[ReaderResult[A], B]
|
||||
|
||||
// Endomorphism represents a function that transforms a value to the same type.
|
||||
// This is an alias for endomorphism.Endomorphism[A].
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the value
|
||||
//
|
||||
// Signature:
|
||||
//
|
||||
// type Endomorphism[A any] = func(A) A
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// // increment is an Endomorphism[int]
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
// Prism is an optic that focuses on a part of a data structure that may or may not be present.
|
||||
// This is an alias for prism.Prism[S, T].
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type
|
||||
// - T: The target type
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // A prism that extracts an int from a string if it's a valid number
|
||||
// intPrism := prism.Prism[string, int]{...}
|
||||
Prism[S, T any] = prism.Prism[S, T]
|
||||
|
||||
// Lens is an optic that focuses on a part of a data structure that is always present.
|
||||
// This is an alias for lens.Lens[S, T].
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type
|
||||
// - T: The target type
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // A lens that focuses on the Name field of a User
|
||||
// nameLens := lens.Lens[User, string]{...}
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
|
||||
// Trampoline represents a computation that can be executed in a stack-safe manner
|
||||
// using tail recursion elimination. This is an alias for tailrec.Trampoline[A, B].
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The input type
|
||||
// - B: The output type
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // A tail-recursive factorial computation
|
||||
// factorial := tailrec.Trampoline[int, int]{...}
|
||||
Trampoline[A, B any] = tailrec.Trampoline[A, B]
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
|
||||
// Predicate represents a function that tests a value and returns a boolean.
|
||||
// This is an alias for predicate.Predicate[A].
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the value to test
|
||||
//
|
||||
// Signature:
|
||||
//
|
||||
// type Predicate[A any] = func(A) bool
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// isPositive := func(x int) bool { return x > 0 }
|
||||
// // isPositive is a Predicate[int]
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
|
||||
// IO represents a side-effectful computation that produces a value of type A.
|
||||
// This is an alias for io.IO[A].
|
||||
//
|
||||
// IMPORTANT: IO operations have side effects (file I/O, network calls, etc.).
|
||||
// Combining IO with ReaderResult makes sense because ReaderResult is already effectful
|
||||
// due to its dependency on context.Context.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the value produced by the IO operation
|
||||
//
|
||||
// Signature:
|
||||
//
|
||||
// type IO[A any] = func() A
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// readConfig := func() Config {
|
||||
// // Side effect: read from file
|
||||
// data, _ := os.ReadFile("config.json")
|
||||
// return parseConfig(data)
|
||||
// }
|
||||
// // readConfig is an IO[Config]
|
||||
IO[A any] = io.IO[A]
|
||||
|
||||
// IOResult represents a side-effectful computation that can fail with an error.
|
||||
// This combines IO (side effects) with Result (error handling).
|
||||
// This is an alias for ioresult.IOResult[A].
|
||||
//
|
||||
// IMPORTANT: IOResult operations have side effects and can fail.
|
||||
// Combining IOResult with ReaderResult makes sense because both are effectful.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the success value
|
||||
//
|
||||
// Signature:
|
||||
//
|
||||
// type IOResult[A any] = func() Result[A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// readConfig := func() result.Result[Config] {
|
||||
// // Side effect: read from file
|
||||
// data, err := os.ReadFile("config.json")
|
||||
// if err != nil {
|
||||
// return result.Error[Config](err)
|
||||
// }
|
||||
// return result.Of(parseConfig(data))
|
||||
// }
|
||||
// // readConfig is an IOResult[Config]
|
||||
IOResult[A any] = ioresult.IOResult[A]
|
||||
)
|
||||
|
||||
@@ -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))
|
||||
|
||||
650
v2/either/applicative_test.go
Normal file
650
v2/either/applicative_test.go
Normal file
@@ -0,0 +1,650 @@
|
||||
// Copyright (c) 2024 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package either
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestApplicativeOf tests the Of operation of the Applicative type class
|
||||
func TestApplicativeOf(t *testing.T) {
|
||||
app := Applicative[error, int, string]()
|
||||
|
||||
t.Run("wraps a value in Right context", func(t *testing.T) {
|
||||
result := app.Of(42)
|
||||
assert.True(t, IsRight(result))
|
||||
assert.Equal(t, 42, GetOrElse(func(error) int { return 0 })(result))
|
||||
})
|
||||
|
||||
t.Run("wraps string value", func(t *testing.T) {
|
||||
app := Applicative[error, string, int]()
|
||||
result := app.Of("hello")
|
||||
assert.True(t, IsRight(result))
|
||||
assert.Equal(t, "hello", GetOrElse(func(error) string { return "" })(result))
|
||||
})
|
||||
|
||||
t.Run("wraps zero value", func(t *testing.T) {
|
||||
result := app.Of(0)
|
||||
assert.True(t, IsRight(result))
|
||||
assert.Equal(t, 0, GetOrElse(func(error) int { return -1 })(result))
|
||||
})
|
||||
|
||||
t.Run("wraps nil pointer", func(t *testing.T) {
|
||||
app := Applicative[error, *int, *string]()
|
||||
var ptr *int = nil
|
||||
result := app.Of(ptr)
|
||||
assert.True(t, IsRight(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMap tests the Map operation of the Applicative type class
|
||||
func TestApplicativeMap(t *testing.T) {
|
||||
app := Applicative[error, int, int]()
|
||||
|
||||
t.Run("maps a function over Right value", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
eitherValue := app.Of(21)
|
||||
result := app.Map(double)(eitherValue)
|
||||
assert.True(t, IsRight(result))
|
||||
assert.Equal(t, 42, GetOrElse(func(error) int { return 0 })(result))
|
||||
})
|
||||
|
||||
t.Run("maps type conversion", func(t *testing.T) {
|
||||
app := Applicative[error, int, string]()
|
||||
eitherValue := app.Of(42)
|
||||
result := app.Map(strconv.Itoa)(eitherValue)
|
||||
assert.True(t, IsRight(result))
|
||||
assert.Equal(t, "42", GetOrElse(func(error) string { return "" })(result))
|
||||
})
|
||||
|
||||
t.Run("maps identity function", func(t *testing.T) {
|
||||
identity := func(x int) int { return x }
|
||||
eitherValue := app.Of(42)
|
||||
result := app.Map(identity)(eitherValue)
|
||||
assert.Equal(t, 42, GetOrElse(func(error) int { return 0 })(result))
|
||||
})
|
||||
|
||||
t.Run("preserves Left on map", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
eitherValue := Left[int](errors.New("error"))
|
||||
result := app.Map(double)(eitherValue)
|
||||
assert.True(t, IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("maps with utils.Double", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
app.Of(21),
|
||||
app.Map(utils.Double),
|
||||
)
|
||||
assert.Equal(t, 42, GetOrElse(func(error) int { return 0 })(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeAp tests the Ap operation of the standard Applicative (fail-fast)
|
||||
func TestApplicativeAp(t *testing.T) {
|
||||
app := Applicative[error, int, int]()
|
||||
|
||||
t.Run("applies wrapped function to wrapped value", func(t *testing.T) {
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
eitherFunc := Right[error](add(10))
|
||||
eitherValue := Right[error](32)
|
||||
result := app.Ap(eitherValue)(eitherFunc)
|
||||
assert.True(t, IsRight(result))
|
||||
assert.Equal(t, 42, GetOrElse(func(error) int { return 0 })(result))
|
||||
})
|
||||
|
||||
t.Run("fails fast when function is Left", func(t *testing.T) {
|
||||
err1 := errors.New("function error")
|
||||
eitherFunc := Left[func(int) int](err1)
|
||||
eitherValue := Right[error](42)
|
||||
result := app.Ap(eitherValue)(eitherFunc)
|
||||
assert.True(t, IsLeft(result))
|
||||
assert.Equal(t, err1, ToError(result))
|
||||
})
|
||||
|
||||
t.Run("fails fast when value is Left", func(t *testing.T) {
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
err2 := errors.New("value error")
|
||||
eitherFunc := Right[error](add(10))
|
||||
eitherValue := Left[int](err2)
|
||||
result := app.Ap(eitherValue)(eitherFunc)
|
||||
assert.True(t, IsLeft(result))
|
||||
assert.Equal(t, err2, ToError(result))
|
||||
})
|
||||
|
||||
t.Run("fails fast when both are Left - returns first error", func(t *testing.T) {
|
||||
err1 := errors.New("function error")
|
||||
err2 := errors.New("value error")
|
||||
eitherFunc := Left[func(int) int](err1)
|
||||
eitherValue := Left[int](err2)
|
||||
result := app.Ap(eitherValue)(eitherFunc)
|
||||
assert.True(t, IsLeft(result))
|
||||
// Should return the first error (function error)
|
||||
assert.Equal(t, err1, ToError(result))
|
||||
})
|
||||
|
||||
t.Run("applies with type conversion", func(t *testing.T) {
|
||||
toStringAndAppend := func(suffix string) func(int) string {
|
||||
return func(n int) string {
|
||||
return strconv.Itoa(n) + suffix
|
||||
}
|
||||
}
|
||||
eitherFunc := Right[error](toStringAndAppend("!"))
|
||||
eitherValue := Right[error](42)
|
||||
result := Ap[string](eitherValue)(eitherFunc)
|
||||
assert.Equal(t, "42!", GetOrElse(func(error) string { return "" })(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeVOf tests the Of operation of ApplicativeV
|
||||
func TestApplicativeVOf(t *testing.T) {
|
||||
sg := S.MakeSemigroup(func(a, b string) string { return a + "; " + b })
|
||||
app := ApplicativeV[string, int, string](sg)
|
||||
|
||||
t.Run("wraps a value in Right context", func(t *testing.T) {
|
||||
result := app.Of(42)
|
||||
assert.True(t, IsRight(result))
|
||||
assert.Equal(t, 42, GetOrElse(func(string) int { return 0 })(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeVMap tests the Map operation of ApplicativeV
|
||||
func TestApplicativeVMap(t *testing.T) {
|
||||
sg := S.MakeSemigroup(func(a, b string) string { return a + "; " + b })
|
||||
app := ApplicativeV[string, int, int](sg)
|
||||
|
||||
t.Run("maps a function over Right value", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
eitherValue := app.Of(21)
|
||||
result := app.Map(double)(eitherValue)
|
||||
assert.True(t, IsRight(result))
|
||||
assert.Equal(t, 42, GetOrElse(func(string) int { return 0 })(result))
|
||||
})
|
||||
|
||||
t.Run("preserves Left on map", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
eitherValue := Left[int]("error")
|
||||
result := app.Map(double)(eitherValue)
|
||||
assert.True(t, IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeVAp tests the Ap operation of ApplicativeV (validation with error accumulation)
|
||||
func TestApplicativeVAp(t *testing.T) {
|
||||
sg := S.MakeSemigroup(func(a, b string) string { return a + "; " + b })
|
||||
app := ApplicativeV[string, int, int](sg)
|
||||
|
||||
t.Run("applies wrapped function to wrapped value", func(t *testing.T) {
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
eitherFunc := Right[string](add(10))
|
||||
eitherValue := Right[string](32)
|
||||
result := app.Ap(eitherValue)(eitherFunc)
|
||||
assert.True(t, IsRight(result))
|
||||
assert.Equal(t, 42, GetOrElse(func(string) int { return 0 })(result))
|
||||
})
|
||||
|
||||
t.Run("returns Left when function is Left", func(t *testing.T) {
|
||||
eitherFunc := Left[func(int) int]("function error")
|
||||
eitherValue := Right[string](42)
|
||||
result := app.Ap(eitherValue)(eitherFunc)
|
||||
assert.True(t, IsLeft(result))
|
||||
leftValue := Fold(F.Identity[string], F.Constant1[int](""))(result)
|
||||
assert.Equal(t, "function error", leftValue)
|
||||
})
|
||||
|
||||
t.Run("returns Left when value is Left", func(t *testing.T) {
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
eitherFunc := Right[string](add(10))
|
||||
eitherValue := Left[int]("value error")
|
||||
result := app.Ap(eitherValue)(eitherFunc)
|
||||
assert.True(t, IsLeft(result))
|
||||
leftValue := Fold(F.Identity[string], F.Constant1[int](""))(result)
|
||||
assert.Equal(t, "value error", leftValue)
|
||||
})
|
||||
|
||||
t.Run("accumulates errors when both are Left", func(t *testing.T) {
|
||||
eitherFunc := Left[func(int) int]("function error")
|
||||
eitherValue := Left[int]("value error")
|
||||
result := app.Ap(eitherValue)(eitherFunc)
|
||||
assert.True(t, IsLeft(result))
|
||||
// Should combine both errors using the semigroup
|
||||
combined := Fold(F.Identity[string], F.Constant1[int](""))(result)
|
||||
assert.Equal(t, "function error; value error", combined)
|
||||
})
|
||||
|
||||
t.Run("accumulates multiple validation errors", func(t *testing.T) {
|
||||
type ValidationErrors []string
|
||||
sg := S.MakeSemigroup(func(a, b ValidationErrors) ValidationErrors {
|
||||
return append(append(ValidationErrors{}, a...), b...)
|
||||
})
|
||||
app := ApplicativeV[ValidationErrors, int, int](sg)
|
||||
|
||||
eitherFunc := Left[func(int) int](ValidationErrors{"error1", "error2"})
|
||||
eitherValue := Left[int](ValidationErrors{"error3", "error4"})
|
||||
result := app.Ap(eitherValue)(eitherFunc)
|
||||
assert.True(t, IsLeft(result))
|
||||
|
||||
errors := Fold(F.Identity[ValidationErrors], F.Constant1[int](ValidationErrors{}))(result)
|
||||
assert.Equal(t, ValidationErrors{"error1", "error2", "error3", "error4"}, errors)
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeLaws tests the applicative functor laws for standard Applicative
|
||||
func TestApplicativeLaws(t *testing.T) {
|
||||
app := Applicative[error, int, int]()
|
||||
|
||||
t.Run("identity law: Ap(Of(id))(v) = v", func(t *testing.T) {
|
||||
identity := func(x int) int { return x }
|
||||
v := app.Of(42)
|
||||
|
||||
left := app.Ap(v)(Of[error](identity))
|
||||
right := v
|
||||
|
||||
assert.Equal(t, GetOrElse(func(error) int { return 0 })(right),
|
||||
GetOrElse(func(error) int { return 0 })(left))
|
||||
})
|
||||
|
||||
t.Run("homomorphism law: Ap(Of(x))(Of(f)) = Of(f(x))", func(t *testing.T) {
|
||||
f := func(x int) int { return x * 2 }
|
||||
x := 21
|
||||
|
||||
left := app.Ap(app.Of(x))(Of[error](f))
|
||||
right := app.Of(f(x))
|
||||
|
||||
assert.Equal(t, GetOrElse(func(error) int { return 0 })(right),
|
||||
GetOrElse(func(error) int { return 0 })(left))
|
||||
})
|
||||
|
||||
t.Run("interchange law: Ap(Of(y))(u) = Ap(u)(Of(f => f(y)))", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
u := Of[error](double)
|
||||
y := 21
|
||||
|
||||
left := app.Ap(app.Of(y))(u)
|
||||
|
||||
// For interchange, we need to apply the value to the function
|
||||
// This test verifies the law holds for the applicative
|
||||
right := Map[error](func(f func(int) int) int { return f(y) })(u)
|
||||
|
||||
assert.Equal(t, GetOrElse(func(error) int { return 0 })(right),
|
||||
GetOrElse(func(error) int { return 0 })(left))
|
||||
})
|
||||
|
||||
t.Run("composition law", func(t *testing.T) {
|
||||
// For Either, we test a simpler version of composition
|
||||
f := func(x int) int { return x * 2 }
|
||||
g := func(x int) int { return x + 10 }
|
||||
x := 16
|
||||
|
||||
// Apply g then f
|
||||
left := F.Pipe2(
|
||||
app.Of(x),
|
||||
app.Map(g),
|
||||
app.Map(f),
|
||||
)
|
||||
|
||||
// Compose f and g, then apply
|
||||
composed := func(x int) int { return f(g(x)) }
|
||||
right := app.Map(composed)(app.Of(x))
|
||||
|
||||
assert.Equal(t, GetOrElse(func(error) int { return 0 })(right),
|
||||
GetOrElse(func(error) int { return 0 })(left))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeVLaws tests the applicative functor laws for ApplicativeV
|
||||
func TestApplicativeVLaws(t *testing.T) {
|
||||
sg := S.MakeSemigroup(func(a, b string) string { return a + "; " + b })
|
||||
app := ApplicativeV[string, int, int](sg)
|
||||
|
||||
t.Run("identity law: Ap(Of(id))(v) = v", func(t *testing.T) {
|
||||
identity := func(x int) int { return x }
|
||||
v := app.Of(42)
|
||||
|
||||
left := app.Ap(v)(Of[string](identity))
|
||||
right := v
|
||||
|
||||
assert.Equal(t, GetOrElse(func(string) int { return 0 })(right),
|
||||
GetOrElse(func(string) int { return 0 })(left))
|
||||
})
|
||||
|
||||
t.Run("homomorphism law: Ap(Of(x))(Of(f)) = Of(f(x))", func(t *testing.T) {
|
||||
f := func(x int) int { return x * 2 }
|
||||
x := 21
|
||||
|
||||
left := app.Ap(app.Of(x))(Of[string](f))
|
||||
right := app.Of(f(x))
|
||||
|
||||
assert.Equal(t, GetOrElse(func(string) int { return 0 })(right),
|
||||
GetOrElse(func(string) int { return 0 })(left))
|
||||
})
|
||||
|
||||
t.Run("interchange law: Ap(Of(y))(u) = Ap(u)(Of(f => f(y)))", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
u := Of[string](double)
|
||||
y := 21
|
||||
|
||||
left := app.Ap(app.Of(y))(u)
|
||||
|
||||
// For interchange, we need to apply the value to the function
|
||||
right := Map[string](func(f func(int) int) int { return f(y) })(u)
|
||||
|
||||
assert.Equal(t, GetOrElse(func(string) int { return 0 })(right),
|
||||
GetOrElse(func(string) int { return 0 })(left))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeComposition tests composition of applicative operations
|
||||
func TestApplicativeComposition(t *testing.T) {
|
||||
app := Applicative[error, int, int]()
|
||||
|
||||
t.Run("composes Map and Of", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
result := F.Pipe1(
|
||||
app.Of(21),
|
||||
app.Map(double),
|
||||
)
|
||||
assert.Equal(t, 42, GetOrElse(func(error) int { return 0 })(result))
|
||||
})
|
||||
|
||||
t.Run("composes multiple Map operations", func(t *testing.T) {
|
||||
app := Applicative[error, int, string]()
|
||||
double := func(x int) int { return x * 2 }
|
||||
toString := func(x int) string { return strconv.Itoa(x) }
|
||||
|
||||
result := F.Pipe2(
|
||||
app.Of(21),
|
||||
Map[error](double),
|
||||
app.Map(toString),
|
||||
)
|
||||
assert.Equal(t, "42", GetOrElse(func(error) string { return "" })(result))
|
||||
})
|
||||
|
||||
t.Run("composes Map and Ap", func(t *testing.T) {
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
|
||||
eitherFunc := F.Pipe1(
|
||||
app.Of(5),
|
||||
Map[error](add),
|
||||
)
|
||||
eitherValue := app.Of(16)
|
||||
|
||||
result := app.Ap(eitherValue)(eitherFunc)
|
||||
assert.Equal(t, 21, GetOrElse(func(error) int { return 0 })(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMultipleArguments tests applying functions with multiple arguments
|
||||
func TestApplicativeMultipleArguments(t *testing.T) {
|
||||
app := Applicative[error, int, int]()
|
||||
|
||||
t.Run("applies curried two-argument function", func(t *testing.T) {
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
|
||||
eitherFunc := F.Pipe1(
|
||||
app.Of(10),
|
||||
Map[error](add),
|
||||
)
|
||||
|
||||
result := app.Ap(app.Of(32))(eitherFunc)
|
||||
assert.Equal(t, 42, GetOrElse(func(error) int { return 0 })(result))
|
||||
})
|
||||
|
||||
t.Run("applies curried three-argument function", func(t *testing.T) {
|
||||
add3 := func(a int) func(int) func(int) int {
|
||||
return func(b int) func(int) int {
|
||||
return func(c int) int {
|
||||
return a + b + c
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eitherFunc1 := F.Pipe1(
|
||||
app.Of(10),
|
||||
Map[error](add3),
|
||||
)
|
||||
|
||||
eitherFunc2 := Ap[func(int) int](app.Of(20))(eitherFunc1)
|
||||
result := Ap[int](app.Of(12))(eitherFunc2)
|
||||
|
||||
assert.Equal(t, 42, GetOrElse(func(error) int { return 0 })(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeInstance tests that Applicative returns a valid instance
|
||||
func TestApplicativeInstance(t *testing.T) {
|
||||
t.Run("returns non-nil instance", func(t *testing.T) {
|
||||
app := Applicative[error, int, string]()
|
||||
assert.NotNil(t, app)
|
||||
})
|
||||
|
||||
t.Run("multiple calls return independent instances", func(t *testing.T) {
|
||||
app1 := Applicative[error, int, string]()
|
||||
app2 := Applicative[error, int, string]()
|
||||
|
||||
result1 := app1.Of(42)
|
||||
result2 := app2.Of(43)
|
||||
|
||||
assert.Equal(t, 42, GetOrElse(func(error) int { return 0 })(result1))
|
||||
assert.Equal(t, 43, GetOrElse(func(error) int { return 0 })(result2))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeVInstance tests that ApplicativeV returns a valid instance
|
||||
func TestApplicativeVInstance(t *testing.T) {
|
||||
sg := S.MakeSemigroup(func(a, b string) string { return a + "; " + b })
|
||||
|
||||
t.Run("returns non-nil instance", func(t *testing.T) {
|
||||
app := ApplicativeV[string, int, string](sg)
|
||||
assert.NotNil(t, app)
|
||||
})
|
||||
|
||||
t.Run("multiple calls return independent instances", func(t *testing.T) {
|
||||
app1 := ApplicativeV[string, int, string](sg)
|
||||
app2 := ApplicativeV[string, int, string](sg)
|
||||
|
||||
result1 := app1.Of(42)
|
||||
result2 := app2.Of(43)
|
||||
|
||||
assert.Equal(t, 42, GetOrElse(func(string) int { return 0 })(result1))
|
||||
assert.Equal(t, 43, GetOrElse(func(string) int { return 0 })(result2))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeWithDifferentTypes tests applicative with various type combinations
|
||||
func TestApplicativeWithDifferentTypes(t *testing.T) {
|
||||
t.Run("int to string", func(t *testing.T) {
|
||||
app := Applicative[error, int, string]()
|
||||
result := app.Map(strconv.Itoa)(app.Of(42))
|
||||
assert.Equal(t, "42", GetOrElse(func(error) string { return "" })(result))
|
||||
})
|
||||
|
||||
t.Run("string to int", func(t *testing.T) {
|
||||
app := Applicative[error, string, int]()
|
||||
toLength := func(s string) int { return len(s) }
|
||||
result := app.Map(toLength)(app.Of("hello"))
|
||||
assert.Equal(t, 5, GetOrElse(func(error) int { return 0 })(result))
|
||||
})
|
||||
|
||||
t.Run("bool to string", func(t *testing.T) {
|
||||
app := Applicative[error, bool, string]()
|
||||
toString := func(b bool) string {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
result := app.Map(toString)(app.Of(true))
|
||||
assert.Equal(t, "true", GetOrElse(func(error) string { return "" })(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeVFormValidationExample demonstrates a realistic form validation scenario
|
||||
func TestApplicativeVFormValidationExample(t *testing.T) {
|
||||
type ValidationErrors []string
|
||||
|
||||
sg := S.MakeSemigroup(func(a, b ValidationErrors) ValidationErrors {
|
||||
return append(append(ValidationErrors{}, a...), b...)
|
||||
})
|
||||
|
||||
validateName := func(name string) Either[ValidationErrors, string] {
|
||||
if len(name) < 3 {
|
||||
return Left[string](ValidationErrors{"Name must be at least 3 characters"})
|
||||
}
|
||||
return Right[ValidationErrors](name)
|
||||
}
|
||||
|
||||
validateAge := func(age int) Either[ValidationErrors, int] {
|
||||
if age < 18 {
|
||||
return Left[int](ValidationErrors{"Must be 18 or older"})
|
||||
}
|
||||
return Right[ValidationErrors](age)
|
||||
}
|
||||
|
||||
validateEmail := func(email string) Either[ValidationErrors, string] {
|
||||
if len(email) == 0 {
|
||||
return Left[string](ValidationErrors{"Email is required"})
|
||||
}
|
||||
return Right[ValidationErrors](email)
|
||||
}
|
||||
|
||||
t.Run("all validations pass", func(t *testing.T) {
|
||||
name := validateName("Alice")
|
||||
age := validateAge(25)
|
||||
email := validateEmail("alice@example.com")
|
||||
|
||||
// Verify all individual validations passed
|
||||
assert.True(t, IsRight(name))
|
||||
assert.True(t, IsRight(age))
|
||||
assert.True(t, IsRight(email))
|
||||
|
||||
// Combine validations - all pass
|
||||
result := F.Pipe2(
|
||||
name,
|
||||
Map[ValidationErrors](func(n string) string { return n }),
|
||||
Map[ValidationErrors](func(n string) string { return n + " validated" }),
|
||||
)
|
||||
|
||||
assert.True(t, IsRight(result))
|
||||
value := GetOrElse(func(ValidationErrors) string { return "" })(result)
|
||||
assert.Equal(t, "Alice validated", value)
|
||||
})
|
||||
|
||||
t.Run("all validations fail - accumulates all errors", func(t *testing.T) {
|
||||
name := validateName("ab")
|
||||
age := validateAge(16)
|
||||
email := validateEmail("")
|
||||
|
||||
// Manually combine errors using the semigroup
|
||||
var allErrors ValidationErrors
|
||||
if IsLeft(name) {
|
||||
allErrors = Fold(F.Identity[ValidationErrors], F.Constant1[string](ValidationErrors{}))(name)
|
||||
}
|
||||
if IsLeft(age) {
|
||||
ageErrors := Fold(F.Identity[ValidationErrors], F.Constant1[int](ValidationErrors{}))(age)
|
||||
allErrors = sg.Concat(allErrors, ageErrors)
|
||||
}
|
||||
if IsLeft(email) {
|
||||
emailErrors := Fold(F.Identity[ValidationErrors], F.Constant1[string](ValidationErrors{}))(email)
|
||||
allErrors = sg.Concat(allErrors, emailErrors)
|
||||
}
|
||||
|
||||
assert.Len(t, allErrors, 3)
|
||||
assert.Contains(t, allErrors, "Name must be at least 3 characters")
|
||||
assert.Contains(t, allErrors, "Must be 18 or older")
|
||||
assert.Contains(t, allErrors, "Email is required")
|
||||
})
|
||||
|
||||
t.Run("partial validation failure", func(t *testing.T) {
|
||||
name := validateName("Alice")
|
||||
age := validateAge(16)
|
||||
email := validateEmail("")
|
||||
|
||||
// Verify name passes
|
||||
assert.True(t, IsRight(name))
|
||||
|
||||
// Manually combine errors using the semigroup
|
||||
var allErrors ValidationErrors
|
||||
if IsLeft(age) {
|
||||
allErrors = Fold(F.Identity[ValidationErrors], F.Constant1[int](ValidationErrors{}))(age)
|
||||
}
|
||||
if IsLeft(email) {
|
||||
emailErrors := Fold(F.Identity[ValidationErrors], F.Constant1[string](ValidationErrors{}))(email)
|
||||
if len(allErrors) > 0 {
|
||||
allErrors = sg.Concat(allErrors, emailErrors)
|
||||
} else {
|
||||
allErrors = emailErrors
|
||||
}
|
||||
}
|
||||
|
||||
assert.Len(t, allErrors, 2)
|
||||
assert.Contains(t, allErrors, "Must be 18 or older")
|
||||
assert.Contains(t, allErrors, "Email is required")
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeVsApplicativeV demonstrates the difference between fail-fast and validation
|
||||
func TestApplicativeVsApplicativeV(t *testing.T) {
|
||||
t.Run("Applicative fails fast", func(t *testing.T) {
|
||||
app := Applicative[error, int, int]()
|
||||
|
||||
err1 := errors.New("error1")
|
||||
err2 := errors.New("error2")
|
||||
|
||||
eitherFunc := Left[func(int) int](err1)
|
||||
eitherValue := Left[int](err2)
|
||||
|
||||
result := app.Ap(eitherValue)(eitherFunc)
|
||||
|
||||
assert.True(t, IsLeft(result))
|
||||
// Only the first error is returned
|
||||
assert.Equal(t, err1, ToError(result))
|
||||
})
|
||||
|
||||
t.Run("ApplicativeV accumulates errors", func(t *testing.T) {
|
||||
sg := S.MakeSemigroup(func(a, b string) string { return a + "; " + b })
|
||||
app := ApplicativeV[string, int, int](sg)
|
||||
|
||||
eitherFunc := Left[func(int) int]("error1")
|
||||
eitherValue := Left[int]("error2")
|
||||
|
||||
result := app.Ap(eitherValue)(eitherFunc)
|
||||
|
||||
assert.True(t, IsLeft(result))
|
||||
// Both errors are accumulated
|
||||
combined := Fold(F.Identity[string], F.Constant1[int](""))(result)
|
||||
assert.Equal(t, "error1; error2", combined)
|
||||
})
|
||||
}
|
||||
@@ -570,3 +570,41 @@ func Flap[E, B, A any](a A) Operator[E, func(A) B, B] {
|
||||
func MonadAlt[E, A any](fa Either[E, A], that Lazy[Either[E, A]]) Either[E, A] {
|
||||
return MonadFold(fa, F.Ignore1of1[E](that), Of[E, A])
|
||||
}
|
||||
|
||||
// Zero returns the zero value of an [Either], which is a Right containing the zero value of type A.
|
||||
// This function is useful as an identity element in monoid operations or for creating an empty Either
|
||||
// in a Right state.
|
||||
//
|
||||
// The returned Either is always a Right value containing the zero value of type A. For reference types
|
||||
// (pointers, slices, maps, channels, functions, interfaces), the zero value is nil. For value types
|
||||
// (numbers, booleans, structs), it's the type's zero value.
|
||||
//
|
||||
// Important: Zero() returns the same value as the default initialization of Either[E, A].
|
||||
// When you declare `var e Either[E, A]` without initialization, it has the same value as Zero[E, A]().
|
||||
//
|
||||
// Note: This differs from creating a Left value, which would represent an error or failure state.
|
||||
// Zero always produces a successful (Right) state with a zero value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Zero Either with int value
|
||||
// e1 := either.Zero[error, int]() // Right(0)
|
||||
//
|
||||
// // Zero Either with string value
|
||||
// e2 := either.Zero[error, string]() // Right("")
|
||||
//
|
||||
// // Zero Either with pointer type
|
||||
// e3 := either.Zero[error, *int]() // Right(nil)
|
||||
//
|
||||
// // Zero equals default initialization
|
||||
// var defaultInit Either[error, int]
|
||||
// zero := either.Zero[error, int]()
|
||||
// assert.Equal(t, defaultInit, zero) // true
|
||||
//
|
||||
// // Verify it's a Right value
|
||||
// e := either.Zero[error, int]()
|
||||
// assert.True(t, either.IsRight(e)) // true
|
||||
// assert.False(t, either.IsLeft(e)) // false
|
||||
func Zero[E, A any]() Either[E, A] {
|
||||
return Either[E, A]{isLeft: false}
|
||||
}
|
||||
|
||||
@@ -119,3 +119,227 @@ func TestStringer(t *testing.T) {
|
||||
var s fmt.Stringer = &e
|
||||
assert.Equal(t, exp, s.String())
|
||||
}
|
||||
|
||||
// TestZeroWithIntegers tests Zero function with integer types
|
||||
func TestZeroWithIntegers(t *testing.T) {
|
||||
e := Zero[error, int]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
assert.False(t, IsLeft(e), "Zero should not create a Left value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Equal(t, 0, value, "Right value should be zero for int")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
}
|
||||
|
||||
// TestZeroWithStrings tests Zero function with string types
|
||||
func TestZeroWithStrings(t *testing.T) {
|
||||
e := Zero[error, string]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
assert.False(t, IsLeft(e), "Zero should not create a Left value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Equal(t, "", value, "Right value should be empty string")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
}
|
||||
|
||||
// TestZeroWithBooleans tests Zero function with boolean types
|
||||
func TestZeroWithBooleans(t *testing.T) {
|
||||
e := Zero[error, bool]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Equal(t, false, value, "Right value should be false for bool")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
}
|
||||
|
||||
// TestZeroWithFloats tests Zero function with float types
|
||||
func TestZeroWithFloats(t *testing.T) {
|
||||
e := Zero[error, float64]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Equal(t, 0.0, value, "Right value should be 0.0 for float64")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
}
|
||||
|
||||
// TestZeroWithPointers tests Zero function with pointer types
|
||||
func TestZeroWithPointers(t *testing.T) {
|
||||
e := Zero[error, *int]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Nil(t, value, "Right value should be nil for pointer type")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
}
|
||||
|
||||
// TestZeroWithSlices tests Zero function with slice types
|
||||
func TestZeroWithSlices(t *testing.T) {
|
||||
e := Zero[error, []int]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Nil(t, value, "Right value should be nil for slice type")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
}
|
||||
|
||||
// TestZeroWithMaps tests Zero function with map types
|
||||
func TestZeroWithMaps(t *testing.T) {
|
||||
e := Zero[error, map[string]int]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Nil(t, value, "Right value should be nil for map type")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
}
|
||||
|
||||
// TestZeroWithStructs tests Zero function with struct types
|
||||
func TestZeroWithStructs(t *testing.T) {
|
||||
type TestStruct struct {
|
||||
Field1 int
|
||||
Field2 string
|
||||
}
|
||||
|
||||
e := Zero[error, TestStruct]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
expected := TestStruct{Field1: 0, Field2: ""}
|
||||
assert.Equal(t, expected, value, "Right value should be zero value for struct")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
}
|
||||
|
||||
// TestZeroWithInterfaces tests Zero function with interface types
|
||||
func TestZeroWithInterfaces(t *testing.T) {
|
||||
e := Zero[error, interface{}]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Nil(t, value, "Right value should be nil for interface type")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
}
|
||||
|
||||
// TestZeroWithCustomErrorType tests Zero function with custom error types
|
||||
func TestZeroWithCustomErrorType(t *testing.T) {
|
||||
type CustomError struct {
|
||||
Code int
|
||||
Message string
|
||||
}
|
||||
|
||||
e := Zero[CustomError, string]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
assert.False(t, IsLeft(e), "Zero should not create a Left value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Equal(t, "", value, "Right value should be empty string")
|
||||
assert.Equal(t, CustomError{Code: 0, Message: ""}, err, "Error should be zero value for CustomError")
|
||||
}
|
||||
|
||||
// TestZeroCanBeUsedWithOtherFunctions tests that Zero Eithers work with other either functions
|
||||
func TestZeroCanBeUsedWithOtherFunctions(t *testing.T) {
|
||||
e := Zero[error, int]()
|
||||
|
||||
// Test with Map
|
||||
mapped := MonadMap(e, func(n int) string {
|
||||
return fmt.Sprintf("%d", n)
|
||||
})
|
||||
assert.True(t, IsRight(mapped), "Mapped Zero should still be Right")
|
||||
value, _ := Unwrap(mapped)
|
||||
assert.Equal(t, "0", value, "Mapped value should be '0'")
|
||||
|
||||
// Test with Chain
|
||||
chained := MonadChain(e, func(n int) Either[error, string] {
|
||||
return Right[error](fmt.Sprintf("value: %d", n))
|
||||
})
|
||||
assert.True(t, IsRight(chained), "Chained Zero should still be Right")
|
||||
chainedValue, _ := Unwrap(chained)
|
||||
assert.Equal(t, "value: 0", chainedValue, "Chained value should be 'value: 0'")
|
||||
|
||||
// Test with Fold
|
||||
folded := MonadFold(e,
|
||||
func(err error) string { return "error" },
|
||||
func(n int) string { return fmt.Sprintf("success: %d", n) },
|
||||
)
|
||||
assert.Equal(t, "success: 0", folded, "Folded value should be 'success: 0'")
|
||||
}
|
||||
|
||||
// TestZeroEquality tests that multiple Zero calls produce equal Eithers
|
||||
func TestZeroEquality(t *testing.T) {
|
||||
e1 := Zero[error, int]()
|
||||
e2 := Zero[error, int]()
|
||||
|
||||
assert.Equal(t, IsRight(e1), IsRight(e2), "Both should be Right")
|
||||
assert.Equal(t, IsLeft(e1), IsLeft(e2), "Both should not be Left")
|
||||
|
||||
v1, err1 := Unwrap(e1)
|
||||
v2, err2 := Unwrap(e2)
|
||||
assert.Equal(t, v1, v2, "Values should be equal")
|
||||
assert.Equal(t, err1, err2, "Errors should be equal")
|
||||
}
|
||||
|
||||
// TestZeroWithComplexTypes tests Zero with more complex nested types
|
||||
func TestZeroWithComplexTypes(t *testing.T) {
|
||||
type ComplexType struct {
|
||||
Nested map[string][]int
|
||||
Ptr *string
|
||||
}
|
||||
|
||||
e := Zero[error, ComplexType]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
expected := ComplexType{Nested: nil, Ptr: nil}
|
||||
assert.Equal(t, expected, value, "Right value should be zero value for complex struct")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
}
|
||||
|
||||
// TestZeroWithOption tests Zero with Option type
|
||||
func TestZeroWithOption(t *testing.T) {
|
||||
e := Zero[error, O.Option[int]]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.True(t, O.IsNone(value), "Right value should be None for Option type")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
}
|
||||
|
||||
// TestZeroIsNotLeft tests that Zero never creates a Left value
|
||||
func TestZeroIsNotLeft(t *testing.T) {
|
||||
// Test with various type combinations
|
||||
e1 := Zero[string, int]()
|
||||
e2 := Zero[error, string]()
|
||||
e3 := Zero[int, bool]()
|
||||
|
||||
assert.False(t, IsLeft(e1), "Zero should never create a Left value")
|
||||
assert.False(t, IsLeft(e2), "Zero should never create a Left value")
|
||||
assert.False(t, IsLeft(e3), "Zero should never create a Left value")
|
||||
|
||||
assert.True(t, IsRight(e1), "Zero should always create a Right value")
|
||||
assert.True(t, IsRight(e2), "Zero should always create a Right value")
|
||||
assert.True(t, IsRight(e3), "Zero should always create a Right value")
|
||||
}
|
||||
|
||||
// TestZeroEqualsDefaultInitialization tests that Zero returns the same value as default initialization
|
||||
func TestZeroEqualsDefaultInitialization(t *testing.T) {
|
||||
// Default initialization of Either
|
||||
var defaultInit Either[error, int]
|
||||
|
||||
// Zero function
|
||||
zero := Zero[error, int]()
|
||||
|
||||
// They should be equal
|
||||
assert.Equal(t, defaultInit, zero, "Zero should equal default initialization")
|
||||
assert.Equal(t, IsRight(defaultInit), IsRight(zero), "Both should be Right")
|
||||
assert.Equal(t, IsLeft(defaultInit), IsLeft(zero), "Both should not be Left")
|
||||
}
|
||||
|
||||
@@ -19,6 +19,64 @@ import (
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
|
||||
// TailRec converts a tail-recursive Kleisli arrow into a stack-safe iterative computation.
|
||||
//
|
||||
// This function enables writing recursive algorithms in a functional style while avoiding
|
||||
// stack overflow errors. It takes a Kleisli arrow that returns a Trampoline wrapped in Either,
|
||||
// and converts it into a regular Kleisli arrow that executes the recursion iteratively.
|
||||
//
|
||||
// The function handles both success and failure cases:
|
||||
// - If any step returns Left[E], the recursion stops and returns that error
|
||||
// - If a step returns Right with Landed=true, the final result is returned
|
||||
// - If a step returns Right with Landed=false, recursion continues with the bounced value
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The error type (Left case)
|
||||
// - A: The input type for each recursive step
|
||||
// - B: The final result type (Right case)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Kleisli arrow that takes an input of type A and returns Either[E, Trampoline[A, B]]
|
||||
// The Trampoline indicates whether to continue (Bounce) or terminate (Land)
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that executes the tail recursion iteratively and returns Either[E, B]
|
||||
//
|
||||
// Example - Factorial with error handling:
|
||||
//
|
||||
// type State struct { n, acc int }
|
||||
//
|
||||
// factorialStep := func(state State) either.Either[string, tailrec.Trampoline[State, int]] {
|
||||
// if state.n < 0 {
|
||||
// return either.Left[tailrec.Trampoline[State, int]]("negative input")
|
||||
// }
|
||||
// if state.n <= 1 {
|
||||
// return either.Right[string](tailrec.Land[State](state.acc))
|
||||
// }
|
||||
// return either.Right[string](tailrec.Bounce[int](State{state.n - 1, state.acc * state.n}))
|
||||
// }
|
||||
//
|
||||
// factorial := either.TailRec(factorialStep)
|
||||
// result := factorial(State{5, 1}) // Right(120)
|
||||
// error := factorial(State{-1, 1}) // Left("negative input")
|
||||
//
|
||||
// Example - Countdown with validation:
|
||||
//
|
||||
// countdown := either.TailRec(func(n int) either.Either[string, tailrec.Trampoline[int, int]] {
|
||||
// if n < 0 {
|
||||
// return either.Left[tailrec.Trampoline[int, int]]("already negative")
|
||||
// }
|
||||
// if n == 0 {
|
||||
// return either.Right[string](tailrec.Land[int](0))
|
||||
// }
|
||||
// return either.Right[string](tailrec.Bounce[int](n - 1))
|
||||
// })
|
||||
//
|
||||
// result := countdown(5) // Right(0)
|
||||
//
|
||||
// The function is stack-safe and can handle arbitrarily deep recursion without
|
||||
// causing stack overflow, as it uses iteration internally rather than actual recursion.
|
||||
//
|
||||
//go:inline
|
||||
func TailRec[E, A, B any](f Kleisli[E, A, tailrec.Trampoline[A, B]]) Kleisli[E, A, B] {
|
||||
return func(a A) Either[E, B] {
|
||||
|
||||
495
v2/either/rec_test.go
Normal file
495
v2/either/rec_test.go
Normal file
@@ -0,0 +1,495 @@
|
||||
// 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 either
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
TR "github.com/IBM/fp-go/v2/tailrec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestTailRecFactorial tests factorial computation with error handling
|
||||
func TestTailRecFactorial(t *testing.T) {
|
||||
type State struct {
|
||||
n int
|
||||
acc int
|
||||
}
|
||||
|
||||
factorialStep := func(state State) Either[string, TR.Trampoline[State, int]] {
|
||||
if state.n < 0 {
|
||||
return Left[TR.Trampoline[State, int]]("negative input not allowed")
|
||||
}
|
||||
if state.n <= 1 {
|
||||
return Right[string](TR.Land[State](state.acc))
|
||||
}
|
||||
return Right[string](TR.Bounce[int](State{state.n - 1, state.acc * state.n}))
|
||||
}
|
||||
|
||||
factorial := TailRec(factorialStep)
|
||||
|
||||
// Test successful computation
|
||||
result := factorial(State{5, 1})
|
||||
assert.Equal(t, Of[string](120), result)
|
||||
|
||||
// Test base case
|
||||
result = factorial(State{0, 1})
|
||||
assert.Equal(t, Of[string](1), result)
|
||||
|
||||
// Test error case
|
||||
result = factorial(State{-1, 1})
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := Unwrap(result)
|
||||
assert.Equal(t, "negative input not allowed", err)
|
||||
}
|
||||
|
||||
// TestTailRecFibonacci tests Fibonacci computation with validation
|
||||
func TestTailRecFibonacci(t *testing.T) {
|
||||
type State struct {
|
||||
n int
|
||||
prev int
|
||||
curr int
|
||||
}
|
||||
|
||||
fibStep := func(state State) Either[string, TR.Trampoline[State, int]] {
|
||||
if state.n < 0 {
|
||||
return Left[TR.Trampoline[State, int]]("negative index")
|
||||
}
|
||||
if state.curr > 1000 {
|
||||
return Left[TR.Trampoline[State, int]](fmt.Sprintf("value too large: %d", state.curr))
|
||||
}
|
||||
if state.n <= 0 {
|
||||
return Right[string](TR.Land[State](state.curr))
|
||||
}
|
||||
return Right[string](TR.Bounce[int](State{state.n - 1, state.curr, state.prev + state.curr}))
|
||||
}
|
||||
|
||||
fib := TailRec(fibStep)
|
||||
|
||||
// Test successful computation
|
||||
result := fib(State{10, 0, 1})
|
||||
assert.Equal(t, Of[string](89), result) // 10th Fibonacci number
|
||||
|
||||
// Test base case
|
||||
result = fib(State{0, 0, 1})
|
||||
assert.Equal(t, Of[string](1), result)
|
||||
|
||||
// Test error case - negative
|
||||
result = fib(State{-1, 0, 1})
|
||||
assert.True(t, IsLeft(result))
|
||||
|
||||
// Test error case - value too large
|
||||
result = fib(State{20, 0, 1})
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := Unwrap(result)
|
||||
assert.Contains(t, err, "value too large")
|
||||
}
|
||||
|
||||
// TestTailRecCountdown tests countdown with validation
|
||||
func TestTailRecCountdown(t *testing.T) {
|
||||
countdownStep := func(n int) Either[string, TR.Trampoline[int, int]] {
|
||||
if n < 0 {
|
||||
return Left[TR.Trampoline[int, int]]("already negative")
|
||||
}
|
||||
if n == 0 {
|
||||
return Right[string](TR.Land[int](0))
|
||||
}
|
||||
return Right[string](TR.Bounce[int](n - 1))
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
|
||||
// Test successful countdown
|
||||
result := countdown(10)
|
||||
assert.Equal(t, Of[string](0), result)
|
||||
|
||||
// Test immediate termination
|
||||
result = countdown(0)
|
||||
assert.Equal(t, Of[string](0), result)
|
||||
|
||||
// Test error case
|
||||
result = countdown(-5)
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := Unwrap(result)
|
||||
assert.Equal(t, "already negative", err)
|
||||
}
|
||||
|
||||
// TestTailRecSumList tests summing a list with error handling
|
||||
func TestTailRecSumList(t *testing.T) {
|
||||
type State struct {
|
||||
list []int
|
||||
sum int
|
||||
}
|
||||
|
||||
sumStep := func(state State) Either[string, TR.Trampoline[State, int]] {
|
||||
if state.sum > 100 {
|
||||
return Left[TR.Trampoline[State, int]](fmt.Sprintf("sum exceeds limit: %d", state.sum))
|
||||
}
|
||||
if A.IsEmpty(state.list) {
|
||||
return Right[string](TR.Land[State](state.sum))
|
||||
}
|
||||
return Right[string](TR.Bounce[int](State{state.list[1:], state.sum + state.list[0]}))
|
||||
}
|
||||
|
||||
sumList := TailRec(sumStep)
|
||||
|
||||
// Test successful sum
|
||||
result := sumList(State{[]int{1, 2, 3, 4, 5}, 0})
|
||||
assert.Equal(t, Of[string](15), result)
|
||||
|
||||
// Test empty list
|
||||
result = sumList(State{[]int{}, 0})
|
||||
assert.Equal(t, Of[string](0), result)
|
||||
|
||||
// Test error case - sum too large
|
||||
result = sumList(State{[]int{50, 60}, 0})
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := Unwrap(result)
|
||||
assert.Contains(t, err, "sum exceeds limit")
|
||||
}
|
||||
|
||||
// TestTailRecImmediateTermination tests immediate termination (Land on first call)
|
||||
func TestTailRecImmediateTermination(t *testing.T) {
|
||||
immediateStep := func(n int) Either[string, TR.Trampoline[int, int]] {
|
||||
return Right[string](TR.Land[int](n * 2))
|
||||
}
|
||||
|
||||
immediate := TailRec(immediateStep)
|
||||
result := immediate(21)
|
||||
|
||||
assert.Equal(t, Of[string](42), result)
|
||||
}
|
||||
|
||||
// TestTailRecImmediateError tests immediate error (Left on first call)
|
||||
func TestTailRecImmediateError(t *testing.T) {
|
||||
immediateErrorStep := func(n int) Either[string, TR.Trampoline[int, int]] {
|
||||
return Left[TR.Trampoline[int, int]]("immediate error")
|
||||
}
|
||||
|
||||
immediateError := TailRec(immediateErrorStep)
|
||||
result := immediateError(42)
|
||||
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := Unwrap(result)
|
||||
assert.Equal(t, "immediate error", err)
|
||||
}
|
||||
|
||||
// TestTailRecStackSafety tests that TailRec handles large iterations without stack overflow
|
||||
func TestTailRecStackSafety(t *testing.T) {
|
||||
countdownStep := func(n int) Either[string, TR.Trampoline[int, int]] {
|
||||
if n <= 0 {
|
||||
return Right[string](TR.Land[int](n))
|
||||
}
|
||||
return Right[string](TR.Bounce[int](n - 1))
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(10000)
|
||||
|
||||
assert.Equal(t, Of[string](0), result)
|
||||
}
|
||||
|
||||
// TestTailRecFindInRange tests finding a value in a range
|
||||
func TestTailRecFindInRange(t *testing.T) {
|
||||
type State struct {
|
||||
current int
|
||||
max int
|
||||
target int
|
||||
}
|
||||
|
||||
findStep := func(state State) Either[string, TR.Trampoline[State, int]] {
|
||||
if state.current > 1000 {
|
||||
return Left[TR.Trampoline[State, int]]("search exceeded maximum iterations")
|
||||
}
|
||||
if state.current >= state.max {
|
||||
return Right[string](TR.Land[State](-1)) // Not found
|
||||
}
|
||||
if state.current == state.target {
|
||||
return Right[string](TR.Land[State](state.current)) // Found
|
||||
}
|
||||
return Right[string](TR.Bounce[int](State{state.current + 1, state.max, state.target}))
|
||||
}
|
||||
|
||||
find := TailRec(findStep)
|
||||
|
||||
// Test found
|
||||
result := find(State{0, 100, 42})
|
||||
assert.Equal(t, Of[string](42), result)
|
||||
|
||||
// Test not found
|
||||
result = find(State{0, 100, 200})
|
||||
assert.Equal(t, Of[string](-1), result)
|
||||
|
||||
// Test error - exceeded iterations
|
||||
result = find(State{0, 2000, 1500})
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := Unwrap(result)
|
||||
assert.Contains(t, err, "exceeded maximum")
|
||||
}
|
||||
|
||||
// TestTailRecCollatzConjecture tests the Collatz conjecture
|
||||
func TestTailRecCollatzConjecture(t *testing.T) {
|
||||
collatzStep := func(n int) Either[string, TR.Trampoline[int, int]] {
|
||||
if n <= 0 {
|
||||
return Left[TR.Trampoline[int, int]]("invalid input: must be positive")
|
||||
}
|
||||
if n == 1 {
|
||||
return Right[string](TR.Land[int](1))
|
||||
}
|
||||
if n%2 == 0 {
|
||||
return Right[string](TR.Bounce[int](n / 2))
|
||||
}
|
||||
return Right[string](TR.Bounce[int](3*n + 1))
|
||||
}
|
||||
|
||||
collatz := TailRec(collatzStep)
|
||||
|
||||
// Test various starting points
|
||||
result := collatz(10)
|
||||
assert.Equal(t, Of[string](1), result)
|
||||
|
||||
result = collatz(27)
|
||||
assert.Equal(t, Of[string](1), result)
|
||||
|
||||
// Test error case
|
||||
result = collatz(0)
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := Unwrap(result)
|
||||
assert.Contains(t, err, "invalid input")
|
||||
}
|
||||
|
||||
// TestTailRecGCD tests greatest common divisor computation
|
||||
func TestTailRecGCD(t *testing.T) {
|
||||
type State struct {
|
||||
a int
|
||||
b int
|
||||
}
|
||||
|
||||
gcdStep := func(state State) Either[string, TR.Trampoline[State, int]] {
|
||||
if state.a < 0 || state.b < 0 {
|
||||
return Left[TR.Trampoline[State, int]]("negative values not allowed")
|
||||
}
|
||||
if state.b == 0 {
|
||||
return Right[string](TR.Land[State](state.a))
|
||||
}
|
||||
return Right[string](TR.Bounce[int](State{state.b, state.a % state.b}))
|
||||
}
|
||||
|
||||
gcd := TailRec(gcdStep)
|
||||
|
||||
// Test successful GCD
|
||||
result := gcd(State{48, 18})
|
||||
assert.Equal(t, Of[string](6), result)
|
||||
|
||||
result = gcd(State{100, 35})
|
||||
assert.Equal(t, Of[string](5), result)
|
||||
|
||||
// Test error case
|
||||
result = gcd(State{-10, 5})
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := Unwrap(result)
|
||||
assert.Contains(t, err, "negative values")
|
||||
}
|
||||
|
||||
// TestTailRecPowerOfTwo tests computing powers of 2
|
||||
func TestTailRecPowerOfTwo(t *testing.T) {
|
||||
type State struct {
|
||||
exponent int
|
||||
result int
|
||||
target int
|
||||
}
|
||||
|
||||
powerStep := func(state State) Either[string, TR.Trampoline[State, int]] {
|
||||
if state.target < 0 {
|
||||
return Left[TR.Trampoline[State, int]]("negative exponent not supported")
|
||||
}
|
||||
if state.exponent >= state.target {
|
||||
return Right[string](TR.Land[State](state.result))
|
||||
}
|
||||
return Right[string](TR.Bounce[int](State{state.exponent + 1, state.result * 2, state.target}))
|
||||
}
|
||||
|
||||
power := TailRec(powerStep)
|
||||
|
||||
// Test 2^10
|
||||
result := power(State{0, 1, 10})
|
||||
assert.Equal(t, Of[string](1024), result)
|
||||
|
||||
// Test 2^0
|
||||
result = power(State{0, 1, 0})
|
||||
assert.Equal(t, Of[string](1), result)
|
||||
|
||||
// Test error case
|
||||
result = power(State{0, 1, -1})
|
||||
assert.True(t, IsLeft(result))
|
||||
}
|
||||
|
||||
// TestTailRecErrorInMiddle tests error occurring in the middle of recursion
|
||||
func TestTailRecErrorInMiddle(t *testing.T) {
|
||||
countdownStep := func(n int) Either[string, TR.Trampoline[int, int]] {
|
||||
if n == 5 {
|
||||
return Left[TR.Trampoline[int, int]]("error at 5")
|
||||
}
|
||||
if n <= 0 {
|
||||
return Right[string](TR.Land[int](n))
|
||||
}
|
||||
return Right[string](TR.Bounce[int](n - 1))
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(10)
|
||||
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := Unwrap(result)
|
||||
assert.Equal(t, "error at 5", err)
|
||||
}
|
||||
|
||||
// TestTailRecMultipleErrorConditions tests multiple error conditions
|
||||
func TestTailRecMultipleErrorConditions(t *testing.T) {
|
||||
type State struct {
|
||||
value int
|
||||
steps int
|
||||
}
|
||||
|
||||
step := func(state State) Either[string, TR.Trampoline[State, int]] {
|
||||
if state.steps > 100 {
|
||||
return Left[TR.Trampoline[State, int]]("too many steps")
|
||||
}
|
||||
if state.value < 0 {
|
||||
return Left[TR.Trampoline[State, int]]("negative value encountered")
|
||||
}
|
||||
if state.value == 0 {
|
||||
return Right[string](TR.Land[State](state.steps))
|
||||
}
|
||||
return Right[string](TR.Bounce[int](State{state.value - 1, state.steps + 1}))
|
||||
}
|
||||
|
||||
counter := TailRec(step)
|
||||
|
||||
// Test successful case
|
||||
result := counter(State{10, 0})
|
||||
assert.Equal(t, Of[string](10), result)
|
||||
|
||||
// Test too many steps error
|
||||
result = counter(State{200, 0})
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := Unwrap(result)
|
||||
assert.Contains(t, err, "too many steps")
|
||||
}
|
||||
|
||||
// TestTailRecWithComplexState tests recursion with complex state
|
||||
func TestTailRecWithComplexState(t *testing.T) {
|
||||
type State struct {
|
||||
numbers []int
|
||||
sum int
|
||||
product int
|
||||
}
|
||||
|
||||
processStep := func(state State) Either[string, TR.Trampoline[State, State]] {
|
||||
if state.product > 10000 {
|
||||
return Left[TR.Trampoline[State, State]]("product overflow")
|
||||
}
|
||||
if A.IsEmpty(state.numbers) {
|
||||
return Right[string](TR.Land[State](state))
|
||||
}
|
||||
head := state.numbers[0]
|
||||
tail := state.numbers[1:]
|
||||
return Right[string](TR.Bounce[State](State{
|
||||
numbers: tail,
|
||||
sum: state.sum + head,
|
||||
product: state.product * head,
|
||||
}))
|
||||
}
|
||||
|
||||
process := TailRec(processStep)
|
||||
|
||||
// Test successful processing
|
||||
result := process(State{[]int{2, 3, 4}, 0, 1})
|
||||
assert.True(t, IsRight(result))
|
||||
finalState, _ := Unwrap(result)
|
||||
assert.Equal(t, 9, finalState.sum)
|
||||
assert.Equal(t, 24, finalState.product)
|
||||
|
||||
// Test overflow error
|
||||
result = process(State{[]int{100, 200, 300}, 0, 1})
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := Unwrap(result)
|
||||
assert.Contains(t, err, "product overflow")
|
||||
}
|
||||
|
||||
// TestTailRecDivisionByZeroProtection tests protection against division by zero
|
||||
func TestTailRecDivisionByZeroProtection(t *testing.T) {
|
||||
type State struct {
|
||||
numerator int
|
||||
denominator int
|
||||
result int
|
||||
}
|
||||
|
||||
divideStep := func(state State) Either[string, TR.Trampoline[State, int]] {
|
||||
if state.denominator == 0 {
|
||||
return Left[TR.Trampoline[State, int]]("division by zero")
|
||||
}
|
||||
if state.numerator < state.denominator {
|
||||
return Right[string](TR.Land[State](state.result))
|
||||
}
|
||||
return Right[string](TR.Bounce[int](State{
|
||||
numerator: state.numerator - state.denominator,
|
||||
denominator: state.denominator,
|
||||
result: state.result + 1,
|
||||
}))
|
||||
}
|
||||
|
||||
divide := TailRec(divideStep)
|
||||
|
||||
// Test successful division
|
||||
result := divide(State{10, 3, 0})
|
||||
assert.Equal(t, Of[string](3), result) // 10 / 3 = 3 (integer division)
|
||||
|
||||
// Test division by zero
|
||||
result = divide(State{10, 0, 0})
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := Unwrap(result)
|
||||
assert.Equal(t, "division by zero", err)
|
||||
}
|
||||
|
||||
// TestTailRecStringProcessing tests recursion with string processing
|
||||
func TestTailRecStringProcessing(t *testing.T) {
|
||||
type State struct {
|
||||
remaining string
|
||||
count int
|
||||
}
|
||||
|
||||
countVowels := func(state State) Either[string, TR.Trampoline[State, int]] {
|
||||
if len(state.remaining) == 0 {
|
||||
return Right[string](TR.Land[State](state.count))
|
||||
}
|
||||
char := state.remaining[0]
|
||||
isVowel := char == 'a' || char == 'e' || char == 'i' || char == 'o' || char == 'u' ||
|
||||
char == 'A' || char == 'E' || char == 'I' || char == 'O' || char == 'U'
|
||||
newCount := state.count
|
||||
if isVowel {
|
||||
newCount++
|
||||
}
|
||||
return Right[string](TR.Bounce[int](State{state.remaining[1:], newCount}))
|
||||
}
|
||||
|
||||
counter := TailRec(countVowels)
|
||||
|
||||
result := counter(State{"hello world", 0})
|
||||
assert.Equal(t, Of[string](3), result) // e, o, o
|
||||
}
|
||||
492
v2/function/bind_test.go
Normal file
492
v2/function/bind_test.go
Normal file
@@ -0,0 +1,492 @@
|
||||
// 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 function
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestBind1st tests the Bind1st function with various scenarios
|
||||
func TestBind1st(t *testing.T) {
|
||||
t.Run("binds first parameter of multiplication", func(t *testing.T) {
|
||||
multiply := func(a, b int) int { return a * b }
|
||||
double := Bind1st(multiply, 2)
|
||||
triple := Bind1st(multiply, 3)
|
||||
|
||||
assert.Equal(t, 10, double(5))
|
||||
assert.Equal(t, 20, double(10))
|
||||
assert.Equal(t, 15, triple(5))
|
||||
assert.Equal(t, 30, triple(10))
|
||||
})
|
||||
|
||||
t.Run("binds first parameter of division", func(t *testing.T) {
|
||||
divide := func(a, b float64) float64 { return a / b }
|
||||
divideBy10 := Bind1st(divide, 10.0)
|
||||
divideBy5 := Bind1st(divide, 5.0)
|
||||
|
||||
assert.Equal(t, 5.0, divideBy10(2.0))
|
||||
assert.Equal(t, 2.0, divideBy10(5.0))
|
||||
assert.Equal(t, 1.0, divideBy5(5.0))
|
||||
})
|
||||
|
||||
t.Run("binds first parameter of subtraction", func(t *testing.T) {
|
||||
subtract := func(a, b int) int { return a - b }
|
||||
subtract10From := Bind1st(subtract, 10)
|
||||
|
||||
assert.Equal(t, 7, subtract10From(3)) // 10 - 3
|
||||
assert.Equal(t, 0, subtract10From(10)) // 10 - 10
|
||||
assert.Equal(t, -5, subtract10From(15)) // 10 - 15
|
||||
})
|
||||
|
||||
t.Run("binds first parameter of string concatenation", func(t *testing.T) {
|
||||
concat := func(a, b string) string { return a + b }
|
||||
addHello := Bind1st(concat, "Hello ")
|
||||
addPrefix := Bind1st(concat, "Prefix: ")
|
||||
|
||||
assert.Equal(t, "Hello World", addHello("World"))
|
||||
assert.Equal(t, "Hello Go", addHello("Go"))
|
||||
assert.Equal(t, "Prefix: Test", addPrefix("Test"))
|
||||
})
|
||||
|
||||
t.Run("binds first parameter with different types", func(t *testing.T) {
|
||||
repeat := func(s string, n int) string {
|
||||
return strings.Repeat(s, n)
|
||||
}
|
||||
repeatX := Bind1st(repeat, "x")
|
||||
repeatAB := Bind1st(repeat, "ab")
|
||||
|
||||
assert.Equal(t, "xxx", repeatX(3))
|
||||
assert.Equal(t, "xxxxx", repeatX(5))
|
||||
assert.Equal(t, "abab", repeatAB(2))
|
||||
})
|
||||
|
||||
t.Run("binds first parameter with complex types", func(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
format := func(p Person, suffix string) string {
|
||||
return fmt.Sprintf("%s (%d) %s", p.Name, p.Age, suffix)
|
||||
}
|
||||
|
||||
alice := Person{Name: "Alice", Age: 30}
|
||||
formatAlice := Bind1st(format, alice)
|
||||
|
||||
assert.Equal(t, "Alice (30) is here", formatAlice("is here"))
|
||||
assert.Equal(t, "Alice (30) says hello", formatAlice("says hello"))
|
||||
})
|
||||
|
||||
t.Run("binds first parameter with slice operations", func(t *testing.T) {
|
||||
appendSlice := func(slice []int, elem int) []int {
|
||||
return append(slice, elem)
|
||||
}
|
||||
|
||||
nums := []int{1, 2, 3}
|
||||
appendToNums := Bind1st(appendSlice, nums)
|
||||
|
||||
result1 := appendToNums(4)
|
||||
assert.Equal(t, []int{1, 2, 3, 4}, result1)
|
||||
|
||||
result2 := appendToNums(5)
|
||||
assert.Equal(t, []int{1, 2, 3, 5}, result2)
|
||||
})
|
||||
|
||||
t.Run("binds first parameter with map operations", func(t *testing.T) {
|
||||
getFromMap := func(m map[string]int, key string) int {
|
||||
return m[key]
|
||||
}
|
||||
|
||||
data := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
getFromData := Bind1st(getFromMap, data)
|
||||
|
||||
assert.Equal(t, 1, getFromData("a"))
|
||||
assert.Equal(t, 2, getFromData("b"))
|
||||
assert.Equal(t, 3, getFromData("c"))
|
||||
})
|
||||
|
||||
t.Run("creates specialized comparison functions", func(t *testing.T) {
|
||||
greaterThan := func(a, b int) bool { return a > b }
|
||||
greaterThan10 := Bind1st(greaterThan, 10)
|
||||
greaterThan5 := Bind1st(greaterThan, 5)
|
||||
|
||||
assert.True(t, greaterThan10(3)) // 10 > 3
|
||||
assert.False(t, greaterThan10(15)) // 10 > 15
|
||||
assert.True(t, greaterThan5(3)) // 5 > 3
|
||||
assert.False(t, greaterThan5(10)) // 5 > 10
|
||||
})
|
||||
}
|
||||
|
||||
// TestBind2nd tests the Bind2nd function with various scenarios
|
||||
func TestBind2nd(t *testing.T) {
|
||||
t.Run("binds second parameter of multiplication", func(t *testing.T) {
|
||||
multiply := func(a, b int) int { return a * b }
|
||||
double := Bind2nd(multiply, 2)
|
||||
triple := Bind2nd(multiply, 3)
|
||||
|
||||
assert.Equal(t, 10, double(5))
|
||||
assert.Equal(t, 20, double(10))
|
||||
assert.Equal(t, 15, triple(5))
|
||||
assert.Equal(t, 30, triple(10))
|
||||
})
|
||||
|
||||
t.Run("binds second parameter of division", func(t *testing.T) {
|
||||
divide := func(a, b float64) float64 { return a / b }
|
||||
halve := Bind2nd(divide, 2.0)
|
||||
third := Bind2nd(divide, 3.0)
|
||||
|
||||
assert.Equal(t, 5.0, halve(10.0))
|
||||
assert.Equal(t, 2.5, halve(5.0))
|
||||
assert.InDelta(t, 3.333, third(10.0), 0.001)
|
||||
})
|
||||
|
||||
t.Run("binds second parameter of subtraction", func(t *testing.T) {
|
||||
subtract := func(a, b int) int { return a - b }
|
||||
decrementBy5 := Bind2nd(subtract, 5)
|
||||
decrementBy10 := Bind2nd(subtract, 10)
|
||||
|
||||
assert.Equal(t, 5, decrementBy5(10)) // 10 - 5
|
||||
assert.Equal(t, 0, decrementBy5(5)) // 5 - 5
|
||||
assert.Equal(t, 0, decrementBy10(10)) // 10 - 10
|
||||
assert.Equal(t, -5, decrementBy10(5)) // 5 - 10
|
||||
})
|
||||
|
||||
t.Run("binds second parameter of string concatenation", func(t *testing.T) {
|
||||
concat := func(a, b string) string { return a + b }
|
||||
addWorld := Bind2nd(concat, " World")
|
||||
addSuffix := Bind2nd(concat, "!")
|
||||
|
||||
assert.Equal(t, "Hello World", addWorld("Hello"))
|
||||
assert.Equal(t, "Goodbye World", addWorld("Goodbye"))
|
||||
assert.Equal(t, "Hello!", addSuffix("Hello"))
|
||||
})
|
||||
|
||||
t.Run("binds second parameter with different types", func(t *testing.T) {
|
||||
repeat := func(s string, n int) string {
|
||||
return strings.Repeat(s, n)
|
||||
}
|
||||
repeatThrice := Bind2nd(repeat, 3)
|
||||
repeatTwice := Bind2nd(repeat, 2)
|
||||
|
||||
assert.Equal(t, "xxx", repeatThrice("x"))
|
||||
assert.Equal(t, "ababab", repeatThrice("ab"))
|
||||
assert.Equal(t, "aa", repeatTwice("a"))
|
||||
})
|
||||
|
||||
t.Run("binds second parameter with complex types", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Debug bool
|
||||
Port int
|
||||
}
|
||||
|
||||
format := func(name string, cfg Config) string {
|
||||
return fmt.Sprintf("%s: debug=%v, port=%d", name, cfg.Debug, cfg.Port)
|
||||
}
|
||||
|
||||
prodConfig := Config{Debug: false, Port: 8080}
|
||||
formatWithProd := Bind2nd(format, prodConfig)
|
||||
|
||||
assert.Equal(t, "API: debug=false, port=8080", formatWithProd("API"))
|
||||
assert.Equal(t, "Web: debug=false, port=8080", formatWithProd("Web"))
|
||||
})
|
||||
|
||||
t.Run("binds second parameter with slice operations", func(t *testing.T) {
|
||||
appendElem := func(slice []int, elem int) []int {
|
||||
return append(slice, elem)
|
||||
}
|
||||
|
||||
append5 := Bind2nd(appendElem, 5)
|
||||
|
||||
result1 := append5([]int{1, 2, 3})
|
||||
assert.Equal(t, []int{1, 2, 3, 5}, result1)
|
||||
|
||||
result2 := append5([]int{10, 20})
|
||||
assert.Equal(t, []int{10, 20, 5}, result2)
|
||||
})
|
||||
|
||||
t.Run("creates specialized comparison functions", func(t *testing.T) {
|
||||
greaterThan := func(a, b int) bool { return a > b }
|
||||
greaterThan10 := Bind2nd(greaterThan, 10)
|
||||
greaterThan5 := Bind2nd(greaterThan, 5)
|
||||
|
||||
assert.False(t, greaterThan10(3)) // 3 > 10
|
||||
assert.True(t, greaterThan10(15)) // 15 > 10
|
||||
assert.False(t, greaterThan5(3)) // 3 > 5
|
||||
assert.True(t, greaterThan5(10)) // 10 > 5
|
||||
})
|
||||
|
||||
t.Run("binds second parameter for power function", func(t *testing.T) {
|
||||
power := func(base, exp float64) float64 {
|
||||
result := 1.0
|
||||
for i := 0; i < int(exp); i++ {
|
||||
result *= base
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
square := Bind2nd(power, 2.0)
|
||||
cube := Bind2nd(power, 3.0)
|
||||
|
||||
assert.Equal(t, 25.0, square(5.0))
|
||||
assert.Equal(t, 100.0, square(10.0))
|
||||
assert.Equal(t, 125.0, cube(5.0))
|
||||
assert.Equal(t, 8.0, cube(2.0))
|
||||
})
|
||||
}
|
||||
|
||||
// TestBind1stVsBind2nd tests the difference between Bind1st and Bind2nd
|
||||
func TestBind1stVsBind2nd(t *testing.T) {
|
||||
t.Run("demonstrates difference with non-commutative operations", func(t *testing.T) {
|
||||
subtract := func(a, b int) int { return a - b }
|
||||
|
||||
// Bind1st: fixes first parameter (a)
|
||||
subtract10From := Bind1st(subtract, 10) // 10 - b
|
||||
assert.Equal(t, 7, subtract10From(3)) // 10 - 3 = 7
|
||||
|
||||
// Bind2nd: fixes second parameter (b)
|
||||
decrementBy10 := Bind2nd(subtract, 10) // a - 10
|
||||
assert.Equal(t, -7, decrementBy10(3)) // 3 - 10 = -7
|
||||
})
|
||||
|
||||
t.Run("demonstrates difference with division", func(t *testing.T) {
|
||||
divide := func(a, b float64) float64 { return a / b }
|
||||
|
||||
// Bind1st: fixes numerator
|
||||
divide10By := Bind1st(divide, 10.0) // 10 / b
|
||||
assert.Equal(t, 5.0, divide10By(2.0)) // 10 / 2 = 5
|
||||
|
||||
// Bind2nd: fixes denominator
|
||||
divideBy10 := Bind2nd(divide, 10.0) // a / 10
|
||||
assert.Equal(t, 0.2, divideBy10(2.0)) // 2 / 10 = 0.2
|
||||
})
|
||||
|
||||
t.Run("demonstrates equivalence with commutative operations", func(t *testing.T) {
|
||||
add := func(a, b int) int { return a + b }
|
||||
|
||||
// For commutative operations, both should give same result
|
||||
add5First := Bind1st(add, 5) // 5 + b
|
||||
add5Second := Bind2nd(add, 5) // a + 5
|
||||
|
||||
assert.Equal(t, 8, add5First(3))
|
||||
assert.Equal(t, 8, add5Second(3))
|
||||
assert.Equal(t, add5First(10), add5Second(10))
|
||||
})
|
||||
}
|
||||
|
||||
// TestSK tests the SK combinator function
|
||||
func TestSK(t *testing.T) {
|
||||
t.Run("returns second argument ignoring first", func(t *testing.T) {
|
||||
assert.Equal(t, "hello", SK(42, "hello"))
|
||||
assert.Equal(t, 100, SK(true, 100))
|
||||
assert.Equal(t, 3.14, SK("test", 3.14))
|
||||
assert.Equal(t, false, SK(123, false))
|
||||
})
|
||||
|
||||
t.Run("works with nil values", func(t *testing.T) {
|
||||
var nilPtr *int
|
||||
assert.Nil(t, SK("ignored", nilPtr))
|
||||
assert.Equal(t, 42, SK(nilPtr, 42))
|
||||
})
|
||||
|
||||
t.Run("works with complex types", func(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
alice := Person{Name: "Alice", Age: 30}
|
||||
bob := Person{Name: "Bob", Age: 25}
|
||||
|
||||
result := SK(alice, bob)
|
||||
assert.Equal(t, "Bob", result.Name)
|
||||
assert.Equal(t, 25, result.Age)
|
||||
})
|
||||
|
||||
t.Run("works with slices", func(t *testing.T) {
|
||||
slice1 := []int{1, 2, 3}
|
||||
slice2 := []string{"a", "b", "c"}
|
||||
|
||||
result := SK(slice1, slice2)
|
||||
assert.Equal(t, []string{"a", "b", "c"}, result)
|
||||
})
|
||||
|
||||
t.Run("works with maps", func(t *testing.T) {
|
||||
map1 := map[string]int{"a": 1}
|
||||
map2 := map[int]string{1: "one"}
|
||||
|
||||
result := SK(map1, map2)
|
||||
assert.Equal(t, map[int]string{1: "one"}, result)
|
||||
})
|
||||
|
||||
t.Run("behaves identically to Second", func(t *testing.T) {
|
||||
// SK should be identical to Second function
|
||||
testCases := []struct {
|
||||
first any
|
||||
second any
|
||||
}{
|
||||
{42, "hello"},
|
||||
{true, 100},
|
||||
{"test", 3.14},
|
||||
{[]int{1, 2}, []string{"a", "b"}},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
assert.Equal(t,
|
||||
Second(tc.first, tc.second),
|
||||
SK(tc.first, tc.second),
|
||||
"SK should behave like Second")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("demonstrates K combinator property", func(t *testing.T) {
|
||||
// SK is the K combinator applied to the second argument
|
||||
// K x y = x, so SK x y = K y x = y
|
||||
// This means SK always returns its second argument
|
||||
|
||||
// Test with various types
|
||||
assert.Equal(t, 42, SK("anything", 42))
|
||||
assert.Equal(t, "result", SK(999, "result"))
|
||||
assert.True(t, SK(false, true))
|
||||
})
|
||||
}
|
||||
|
||||
// TestBindComposition tests composition of bind operations
|
||||
func TestBindComposition(t *testing.T) {
|
||||
t.Run("composes multiple Bind1st operations", func(t *testing.T) {
|
||||
add := func(a, b int) int { return a + b }
|
||||
multiply := func(a, b int) int { return a * b }
|
||||
|
||||
add5 := Bind1st(add, 5)
|
||||
double := Bind1st(multiply, 2)
|
||||
|
||||
// Compose: first add 5, then double
|
||||
result := double(add5(3)) // (3 + 5) * 2 = 16
|
||||
assert.Equal(t, 16, result)
|
||||
})
|
||||
|
||||
t.Run("composes Bind1st and Bind2nd", func(t *testing.T) {
|
||||
subtract := func(a, b int) int { return a - b }
|
||||
|
||||
subtract10From := Bind1st(subtract, 10) // 10 - b
|
||||
decrementBy5 := Bind2nd(subtract, 5) // a - 5
|
||||
|
||||
// Apply both transformations
|
||||
result1 := decrementBy5(subtract10From(3)) // (10 - 3) - 5 = 2
|
||||
assert.Equal(t, 2, result1)
|
||||
|
||||
result2 := subtract10From(decrementBy5(8)) // 10 - (8 - 5) = 7
|
||||
assert.Equal(t, 7, result2)
|
||||
})
|
||||
|
||||
t.Run("creates pipeline with bound functions", func(t *testing.T) {
|
||||
multiply := func(a, b int) int { return a * b }
|
||||
add := func(a, b int) int { return a + b }
|
||||
|
||||
double := Bind2nd(multiply, 2)
|
||||
add10 := Bind2nd(add, 10)
|
||||
|
||||
// Pipeline: input -> double -> add10
|
||||
pipeline := func(n int) int {
|
||||
return add10(double(n))
|
||||
}
|
||||
|
||||
assert.Equal(t, 20, pipeline(5)) // (5 * 2) + 10 = 20
|
||||
assert.Equal(t, 30, pipeline(10)) // (10 * 2) + 10 = 30
|
||||
})
|
||||
}
|
||||
|
||||
// TestBindWithHigherOrderFunctions tests bind with higher-order functions
|
||||
func TestBindWithHigherOrderFunctions(t *testing.T) {
|
||||
t.Run("binds function parameter", func(t *testing.T) {
|
||||
applyTwice := func(f func(int) int, n int) int {
|
||||
return f(f(n))
|
||||
}
|
||||
|
||||
increment := func(n int) int { return n + 1 }
|
||||
applyIncrementTwice := Bind1st(applyTwice, increment)
|
||||
|
||||
assert.Equal(t, 7, applyIncrementTwice(5)) // increment(increment(5)) = 7
|
||||
})
|
||||
|
||||
t.Run("binds value for higher-order function", func(t *testing.T) {
|
||||
applyFunc := func(f func(int) int, n int) int {
|
||||
return f(n)
|
||||
}
|
||||
|
||||
applyTo10 := Bind2nd(applyFunc, 10)
|
||||
|
||||
double := func(n int) int { return n * 2 }
|
||||
square := func(n int) int { return n * n }
|
||||
|
||||
assert.Equal(t, 20, applyTo10(double)) // double(10) = 20
|
||||
assert.Equal(t, 100, applyTo10(square)) // square(10) = 100
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkBind1st benchmarks the Bind1st function
|
||||
func BenchmarkBind1st(b *testing.B) {
|
||||
multiply := func(a, b int) int { return a * b }
|
||||
double := Bind1st(multiply, 2)
|
||||
|
||||
b.Run("direct call", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = multiply(2, i)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("bound function", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = double(i)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkBind2nd benchmarks the Bind2nd function
|
||||
func BenchmarkBind2nd(b *testing.B) {
|
||||
multiply := func(a, b int) int { return a * b }
|
||||
double := Bind2nd(multiply, 2)
|
||||
|
||||
b.Run("direct call", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = multiply(i, 2)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("bound function", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = double(i)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkSK benchmarks the SK combinator
|
||||
func BenchmarkSK(b *testing.B) {
|
||||
b.Run("SK with ints", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = SK(i, i+1)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Second with ints", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = Second(i, i+1)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -19,23 +19,265 @@ import (
|
||||
G "github.com/IBM/fp-go/v2/function/generic"
|
||||
)
|
||||
|
||||
// Memoize converts a unary function into a unary function that caches the value depending on the parameter
|
||||
// Memoize converts a unary function into a memoized version that caches computed values.
|
||||
//
|
||||
// Behavior:
|
||||
// - On first call with a given input, the function executes and the result is cached
|
||||
// - Subsequent calls with the same input return the cached result without re-execution
|
||||
// - The cache uses the input parameter directly as the key (must be comparable)
|
||||
// - The cache is thread-safe using mutex locks
|
||||
// - The cache has no size limit and grows unbounded
|
||||
// - Each unique input creates a new cache entry that persists for the lifetime of the memoized function
|
||||
//
|
||||
// Implementation Details:
|
||||
// - Uses an internal map[K]func()T to store lazy values
|
||||
// - The cached value is wrapped in a lazy function to defer computation until needed
|
||||
// - Lock is held only to access the cache map, not during value computation
|
||||
// - This allows concurrent computations for different keys
|
||||
//
|
||||
// Type Parameters:
|
||||
// - K: The type of the function parameter, must be comparable (used as cache key)
|
||||
// - T: The return type of the function
|
||||
//
|
||||
// Parameters:
|
||||
// - f: The function to memoize
|
||||
//
|
||||
// Returns:
|
||||
// - A memoized version of the function that caches results by parameter value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Expensive computation
|
||||
// expensiveCalc := func(n int) int {
|
||||
// time.Sleep(100 * time.Millisecond)
|
||||
// return n * n
|
||||
// }
|
||||
//
|
||||
// // Memoize to avoid redundant calculations
|
||||
// memoized := Memoize(expensiveCalc)
|
||||
// result1 := memoized(5) // Takes 100ms, computes and caches 25
|
||||
// result2 := memoized(5) // Instant, returns cached 25
|
||||
// result3 := memoized(10) // Takes 100ms, computes and caches 100
|
||||
//
|
||||
// Note: The cache grows unbounded. For bounded caches, use CacheCallback with a custom cache implementation.
|
||||
func Memoize[K comparable, T any](f func(K) T) func(K) T {
|
||||
return G.Memoize(f)
|
||||
}
|
||||
|
||||
// ContramapMemoize converts a unary function into a unary function that caches the value depending on the parameter
|
||||
// ContramapMemoize creates a higher-order function that memoizes functions using a custom key extraction strategy.
|
||||
//
|
||||
// Behavior:
|
||||
// - Allows caching based on a derived key rather than the full input parameter
|
||||
// - The key extraction function (kf) determines what constitutes a cache hit
|
||||
// - Two inputs that produce the same key will share the same cached result
|
||||
// - This enables caching for non-comparable types by extracting comparable keys
|
||||
// - The cache is thread-safe and unbounded
|
||||
//
|
||||
// Use Cases:
|
||||
// - Cache by a subset of struct fields (e.g., User.ID instead of entire User)
|
||||
// - Cache by a computed property (e.g., string length, hash value)
|
||||
// - Normalize inputs before caching (e.g., lowercase strings, rounded numbers)
|
||||
//
|
||||
// Implementation Details:
|
||||
// - Internally uses the same caching mechanism as Memoize
|
||||
// - The key function is applied to each input before cache lookup
|
||||
// - Returns a function transformer that can be applied to any function with matching signature
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The return type of the function to be memoized
|
||||
// - A: The input type of the function to be memoized
|
||||
// - K: The type of the cache key, must be comparable
|
||||
//
|
||||
// Parameters:
|
||||
// - kf: A function that extracts a cache key from the input parameter
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a function (A) -> T and returns its memoized version
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type User struct {
|
||||
// ID int
|
||||
// Name string
|
||||
// Email string
|
||||
// }
|
||||
//
|
||||
// // Cache by user ID only, ignoring other fields
|
||||
// cacheByID := ContramapMemoize[string, User, int](func(u User) int {
|
||||
// return u.ID
|
||||
// })
|
||||
//
|
||||
// getUserData := func(u User) string {
|
||||
// // Expensive database lookup
|
||||
// return fmt.Sprintf("Data for user %d", u.ID)
|
||||
// }
|
||||
//
|
||||
// memoized := cacheByID(getUserData)
|
||||
// result1 := memoized(User{ID: 1, Name: "Alice", Email: "a@example.com"}) // Computed
|
||||
// result2 := memoized(User{ID: 1, Name: "Bob", Email: "b@example.com"}) // Cached (same ID)
|
||||
// result3 := memoized(User{ID: 2, Name: "Alice", Email: "a@example.com"}) // Computed (different ID)
|
||||
func ContramapMemoize[T, A any, K comparable](kf func(A) K) func(func(A) T) func(A) T {
|
||||
return G.ContramapMemoize[func(A) T](kf)
|
||||
}
|
||||
|
||||
// CacheCallback converts a unary function into a unary function that caches the value depending on the parameter
|
||||
// CacheCallback creates a higher-order function that memoizes functions using a custom cache implementation.
|
||||
//
|
||||
// Behavior:
|
||||
// - Provides complete control over caching strategy through the getOrCreate callback
|
||||
// - Separates cache key extraction (kf) from cache storage (getOrCreate)
|
||||
// - The getOrCreate function receives a key and a lazy value generator
|
||||
// - The cache implementation decides when to store, evict, or retrieve values
|
||||
// - Enables advanced caching strategies: LRU, LFU, TTL, bounded size, etc.
|
||||
//
|
||||
// How It Works:
|
||||
// 1. When the memoized function is called with input A:
|
||||
// 2. The key function (kf) extracts a cache key K from A
|
||||
// 3. A lazy value generator is created that will compute f(A) when called
|
||||
// 4. The getOrCreate callback is invoked with the key and lazy generator
|
||||
// 5. The cache implementation returns a lazy value (either cached or newly created)
|
||||
// 6. The lazy value is evaluated to produce the final result T
|
||||
//
|
||||
// Cache Implementation Contract:
|
||||
// - getOrCreate receives: (key K, generator func() func() T)
|
||||
// - getOrCreate returns: func() T (a lazy value)
|
||||
// - The generator creates a new lazy value when called
|
||||
// - The cache should store and return lazy values, not final results
|
||||
// - This allows deferred computation and proper lazy evaluation
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The return type of the function to be memoized
|
||||
// - A: The input type of the function to be memoized
|
||||
// - K: The type of the cache key, must be comparable
|
||||
//
|
||||
// Parameters:
|
||||
// - kf: A function that extracts a cache key from the input parameter
|
||||
// - getOrCreate: A cache implementation that stores and retrieves lazy values
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a function (A) -> T and returns its memoized version
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a bounded LRU cache (max 100 items)
|
||||
// lruCache := func() func(int, func() func() string) func() string {
|
||||
// cache := make(map[int]func() string)
|
||||
// keys := []int{}
|
||||
// var mu sync.Mutex
|
||||
// maxSize := 100
|
||||
//
|
||||
// return func(k int, gen func() func() string) func() string {
|
||||
// mu.Lock()
|
||||
// defer mu.Unlock()
|
||||
//
|
||||
// if existing, ok := cache[k]; ok {
|
||||
// return existing // Cache hit
|
||||
// }
|
||||
//
|
||||
// // Evict oldest if at capacity
|
||||
// if len(keys) >= maxSize {
|
||||
// delete(cache, keys[0])
|
||||
// keys = keys[1:]
|
||||
// }
|
||||
//
|
||||
// // Create and store new lazy value
|
||||
// value := gen()
|
||||
// cache[k] = value
|
||||
// keys = append(keys, k)
|
||||
// return value
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Use custom cache with memoization
|
||||
// memoizer := CacheCallback[string, int, int](
|
||||
// Identity[int], // Use input as key
|
||||
// lruCache(),
|
||||
// )
|
||||
//
|
||||
// expensiveFunc := func(n int) string {
|
||||
// time.Sleep(100 * time.Millisecond)
|
||||
// return fmt.Sprintf("Result: %d", n)
|
||||
// }
|
||||
//
|
||||
// memoized := memoizer(expensiveFunc)
|
||||
// result := memoized(42) // Computed and cached
|
||||
// result = memoized(42) // Retrieved from cache
|
||||
//
|
||||
// See also: SingleElementCache for a simple bounded cache implementation.
|
||||
func CacheCallback[
|
||||
T, A any, K comparable](kf func(A) K, getOrCreate func(K, func() func() T) func() T) func(func(A) T) func(A) T {
|
||||
return G.CacheCallback[func(func(A) T) func(A) T](kf, getOrCreate)
|
||||
}
|
||||
|
||||
// SingleElementCache creates a cache function for use with the [CacheCallback] method that has a maximum capacity of one single item
|
||||
// SingleElementCache creates a thread-safe cache implementation that stores at most one element.
|
||||
//
|
||||
// Behavior:
|
||||
// - Stores only the most recently accessed key-value pair
|
||||
// - When a new key is accessed, it replaces the previous cached entry
|
||||
// - If the same key is accessed again, the cached value is returned
|
||||
// - Thread-safe: uses mutex to protect concurrent access
|
||||
// - Memory-efficient: constant O(1) space regardless of usage
|
||||
//
|
||||
// How It Works:
|
||||
// 1. Initially, the cache is empty (hasKey = false)
|
||||
// 2. On first access with key K1:
|
||||
// - Calls the generator to create a lazy value
|
||||
// - Stores K1 and the lazy value
|
||||
// - Returns the lazy value
|
||||
// 3. On subsequent access with same key K1:
|
||||
// - Returns the stored lazy value without calling generator
|
||||
// 4. On access with different key K2:
|
||||
// - Calls the generator to create a new lazy value
|
||||
// - Replaces K1 with K2 and updates the stored lazy value
|
||||
// - Returns the new lazy value
|
||||
// 5. If K1 is accessed again, it's treated as a new key (cache miss)
|
||||
//
|
||||
// Use Cases:
|
||||
// - Sequential processing where the same key is accessed multiple times in a row
|
||||
// - Memory-constrained environments where unbounded caches are not feasible
|
||||
// - Scenarios where only the most recent computation needs caching
|
||||
// - Testing or debugging with controlled cache behavior
|
||||
//
|
||||
// Important Notes:
|
||||
// - The cache stores the lazy value (func() T), not the computed result
|
||||
// - Each time the returned lazy value is called, it may recompute (depends on lazy implementation)
|
||||
// - For true result caching, combine with lazy memoization (as done in CacheCallback)
|
||||
// - Alternating between two keys will cause constant cache misses
|
||||
//
|
||||
// Type Parameters:
|
||||
// - K: The type of the cache key, must be comparable
|
||||
// - T: The type of the cached value
|
||||
//
|
||||
// Returns:
|
||||
// - A cache function suitable for use with CacheCallback
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a single-element cache
|
||||
// cache := SingleElementCache[int, string]()
|
||||
//
|
||||
// // Use with CacheCallback
|
||||
// memoizer := CacheCallback[string, int, int](
|
||||
// Identity[int], // Use input as key
|
||||
// cache,
|
||||
// )
|
||||
//
|
||||
// expensiveFunc := func(n int) string {
|
||||
// time.Sleep(100 * time.Millisecond)
|
||||
// return fmt.Sprintf("Result: %d", n)
|
||||
// }
|
||||
//
|
||||
// memoized := memoizer(expensiveFunc)
|
||||
// result1 := memoized(42) // Computed (100ms) and cached
|
||||
// result2 := memoized(42) // Instant - returns cached value
|
||||
// result3 := memoized(99) // Computed (100ms) - replaces cache entry for 42
|
||||
// result4 := memoized(99) // Instant - returns cached value
|
||||
// result5 := memoized(42) // Computed (100ms) - cache was replaced, must recompute
|
||||
//
|
||||
// Performance Characteristics:
|
||||
// - Space: O(1) - stores exactly one key-value pair
|
||||
// - Time: O(1) - cache lookup and update are constant time
|
||||
// - Best case: Same key accessed repeatedly (100% hit rate)
|
||||
// - Worst case: Alternating keys (0% hit rate)
|
||||
func SingleElementCache[K comparable, T any]() func(K, func() func() T) func() T {
|
||||
return G.SingleElementCache[func() func() T, K]()
|
||||
}
|
||||
|
||||
@@ -17,54 +17,601 @@ package function
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
var count int
|
||||
// TestMemoize tests the Memoize function
|
||||
func TestMemoize(t *testing.T) {
|
||||
t.Run("caches computed values", func(t *testing.T) {
|
||||
callCount := 0
|
||||
expensive := func(n int) int {
|
||||
callCount++
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
return n * 2
|
||||
}
|
||||
|
||||
withSideEffect := func(n int) int {
|
||||
count++
|
||||
return n
|
||||
}
|
||||
memoized := Memoize(expensive)
|
||||
|
||||
cached := Memoize(withSideEffect)
|
||||
// First call should compute
|
||||
result1 := memoized(5)
|
||||
assert.Equal(t, 10, result1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
assert.Equal(t, 0, count)
|
||||
// Second call with same input should use cache
|
||||
result2 := memoized(5)
|
||||
assert.Equal(t, 10, result2)
|
||||
assert.Equal(t, 1, callCount, "should not recompute for cached value")
|
||||
|
||||
assert.Equal(t, 10, cached(10))
|
||||
assert.Equal(t, 1, count)
|
||||
// Different input should compute again
|
||||
result3 := memoized(10)
|
||||
assert.Equal(t, 20, result3)
|
||||
assert.Equal(t, 2, callCount)
|
||||
|
||||
assert.Equal(t, 10, cached(10))
|
||||
assert.Equal(t, 1, count)
|
||||
// Original input should still be cached
|
||||
result4 := memoized(5)
|
||||
assert.Equal(t, 10, result4)
|
||||
assert.Equal(t, 2, callCount, "should still use cached value")
|
||||
})
|
||||
|
||||
assert.Equal(t, 20, cached(20))
|
||||
assert.Equal(t, 2, count)
|
||||
t.Run("works with string keys", func(t *testing.T) {
|
||||
callCount := 0
|
||||
toUpper := func(s string) string {
|
||||
callCount++
|
||||
return fmt.Sprintf("UPPER_%s", s)
|
||||
}
|
||||
|
||||
assert.Equal(t, 20, cached(20))
|
||||
assert.Equal(t, 2, count)
|
||||
memoized := Memoize(toUpper)
|
||||
|
||||
assert.Equal(t, 10, cached(10))
|
||||
assert.Equal(t, 2, count)
|
||||
result1 := memoized("hello")
|
||||
assert.Equal(t, "UPPER_hello", result1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
result2 := memoized("hello")
|
||||
assert.Equal(t, "UPPER_hello", result2)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
result3 := memoized("world")
|
||||
assert.Equal(t, "UPPER_world", result3)
|
||||
assert.Equal(t, 2, callCount)
|
||||
})
|
||||
|
||||
t.Run("is thread-safe", func(t *testing.T) {
|
||||
var callCount int32
|
||||
expensive := func(n int) int {
|
||||
atomic.AddInt32(&callCount, 1)
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
return n * n
|
||||
}
|
||||
|
||||
memoized := Memoize(expensive)
|
||||
|
||||
// Run concurrent calls with same input
|
||||
var wg sync.WaitGroup
|
||||
results := make([]int, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
results[idx] = memoized(7)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// All results should be the same
|
||||
for _, result := range results {
|
||||
assert.Equal(t, 49, result)
|
||||
}
|
||||
|
||||
// Function should be called at least once, but possibly more due to race
|
||||
// (the cache is eventually consistent)
|
||||
assert.Greater(t, atomic.LoadInt32(&callCount), int32(0))
|
||||
})
|
||||
|
||||
t.Run("handles zero values correctly", func(t *testing.T) {
|
||||
callCount := 0
|
||||
identity := func(n int) int {
|
||||
callCount++
|
||||
return n
|
||||
}
|
||||
|
||||
memoized := Memoize(identity)
|
||||
|
||||
result1 := memoized(0)
|
||||
assert.Equal(t, 0, result1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
result2 := memoized(0)
|
||||
assert.Equal(t, 0, result2)
|
||||
assert.Equal(t, 1, callCount, "should cache zero value")
|
||||
})
|
||||
|
||||
t.Run("caches multiple different values", func(t *testing.T) {
|
||||
callCount := 0
|
||||
square := func(n int) int {
|
||||
callCount++
|
||||
return n * n
|
||||
}
|
||||
|
||||
memoized := Memoize(square)
|
||||
|
||||
// Cache multiple values
|
||||
assert.Equal(t, 4, memoized(2))
|
||||
assert.Equal(t, 9, memoized(3))
|
||||
assert.Equal(t, 16, memoized(4))
|
||||
assert.Equal(t, 3, callCount)
|
||||
|
||||
// All should be cached
|
||||
assert.Equal(t, 4, memoized(2))
|
||||
assert.Equal(t, 9, memoized(3))
|
||||
assert.Equal(t, 16, memoized(4))
|
||||
assert.Equal(t, 3, callCount, "all values should be cached")
|
||||
})
|
||||
}
|
||||
|
||||
// TestContramapMemoize tests the ContramapMemoize function
|
||||
func TestContramapMemoize(t *testing.T) {
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("caches by extracted key", func(t *testing.T) {
|
||||
callCount := 0
|
||||
getUserData := func(u User) string {
|
||||
callCount++
|
||||
return fmt.Sprintf("Data for user %d: %s", u.ID, u.Name)
|
||||
}
|
||||
|
||||
// Cache by ID only
|
||||
cacheByID := ContramapMemoize[string](func(u User) int {
|
||||
return u.ID
|
||||
})
|
||||
|
||||
memoized := cacheByID(getUserData)
|
||||
|
||||
user1 := User{ID: 1, Name: "Alice", Age: 30}
|
||||
result1 := memoized(user1)
|
||||
assert.Equal(t, "Data for user 1: Alice", result1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
// Same ID, different name - should use cache
|
||||
user2 := User{ID: 1, Name: "Bob", Age: 25}
|
||||
result2 := memoized(user2)
|
||||
assert.Equal(t, "Data for user 1: Alice", result2, "should return cached result")
|
||||
assert.Equal(t, 1, callCount, "should not recompute")
|
||||
|
||||
// Different ID - should compute
|
||||
user3 := User{ID: 2, Name: "Charlie", Age: 35}
|
||||
result3 := memoized(user3)
|
||||
assert.Equal(t, "Data for user 2: Charlie", result3)
|
||||
assert.Equal(t, 2, callCount)
|
||||
})
|
||||
|
||||
t.Run("works with string key extraction", func(t *testing.T) {
|
||||
type Product struct {
|
||||
SKU string
|
||||
Name string
|
||||
Price float64
|
||||
}
|
||||
|
||||
callCount := 0
|
||||
getPrice := func(p Product) float64 {
|
||||
callCount++
|
||||
return p.Price * 1.1 // Add 10% markup
|
||||
}
|
||||
|
||||
cacheBySKU := ContramapMemoize[float64](func(p Product) string {
|
||||
return p.SKU
|
||||
})
|
||||
|
||||
memoized := cacheBySKU(getPrice)
|
||||
|
||||
prod1 := Product{SKU: "ABC123", Name: "Widget", Price: 100.0}
|
||||
result1 := memoized(prod1)
|
||||
assert.InDelta(t, 110.0, result1, 0.01)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
// Same SKU, different price - should use cached result
|
||||
prod2 := Product{SKU: "ABC123", Name: "Widget", Price: 200.0}
|
||||
result2 := memoized(prod2)
|
||||
assert.InDelta(t, 110.0, result2, 0.01, "should use cached value")
|
||||
assert.Equal(t, 1, callCount)
|
||||
})
|
||||
|
||||
t.Run("can use complex key extraction", func(t *testing.T) {
|
||||
type Request struct {
|
||||
Method string
|
||||
Path string
|
||||
Body string
|
||||
}
|
||||
|
||||
callCount := 0
|
||||
processRequest := func(r Request) string {
|
||||
callCount++
|
||||
return fmt.Sprintf("Processed: %s %s", r.Method, r.Path)
|
||||
}
|
||||
|
||||
// Cache by method and path, ignore body
|
||||
cacheByMethodPath := ContramapMemoize[string](func(r Request) string {
|
||||
return r.Method + ":" + r.Path
|
||||
})
|
||||
|
||||
memoized := cacheByMethodPath(processRequest)
|
||||
|
||||
req1 := Request{Method: "GET", Path: "/api/users", Body: "body1"}
|
||||
result1 := memoized(req1)
|
||||
assert.Equal(t, "Processed: GET /api/users", result1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
// Same method and path, different body - should use cache
|
||||
req2 := Request{Method: "GET", Path: "/api/users", Body: "body2"}
|
||||
result2 := memoized(req2)
|
||||
assert.Equal(t, "Processed: GET /api/users", result2)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
// Different path - should compute
|
||||
req3 := Request{Method: "GET", Path: "/api/posts", Body: "body1"}
|
||||
result3 := memoized(req3)
|
||||
assert.Equal(t, "Processed: GET /api/posts", result3)
|
||||
assert.Equal(t, 2, callCount)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCacheCallback tests the CacheCallback function
|
||||
func TestCacheCallback(t *testing.T) {
|
||||
t.Run("works with custom cache implementation", func(t *testing.T) {
|
||||
// Create a simple bounded cache (max 2 items)
|
||||
boundedCache := func() func(int, func() func() string) func() string {
|
||||
cache := make(map[int]func() string)
|
||||
keys := []int{}
|
||||
var mu sync.Mutex
|
||||
|
||||
return func(k int, gen func() func() string) func() string {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if existing, ok := cache[k]; ok {
|
||||
return existing
|
||||
}
|
||||
|
||||
// Evict oldest if at capacity
|
||||
if len(keys) >= 2 {
|
||||
oldestKey := keys[0]
|
||||
delete(cache, oldestKey)
|
||||
keys = keys[1:]
|
||||
}
|
||||
|
||||
value := gen()
|
||||
cache[k] = value
|
||||
keys = append(keys, k)
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
callCount := 0
|
||||
expensive := func(n int) string {
|
||||
callCount++
|
||||
return fmt.Sprintf("Result: %d", n)
|
||||
}
|
||||
|
||||
memoizer := CacheCallback(
|
||||
Identity[int],
|
||||
boundedCache(),
|
||||
)
|
||||
|
||||
memoized := memoizer(expensive)
|
||||
|
||||
// Cache first two values
|
||||
result1 := memoized(1)
|
||||
assert.Equal(t, "Result: 1", result1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
result2 := memoized(2)
|
||||
assert.Equal(t, "Result: 2", result2)
|
||||
assert.Equal(t, 2, callCount)
|
||||
|
||||
// Both should be cached
|
||||
memoized(1)
|
||||
memoized(2)
|
||||
assert.Equal(t, 2, callCount)
|
||||
|
||||
// Third value should evict first
|
||||
result3 := memoized(3)
|
||||
assert.Equal(t, "Result: 3", result3)
|
||||
assert.Equal(t, 3, callCount)
|
||||
|
||||
// First value should be recomputed (evicted)
|
||||
// Note: The cache stores lazy generators, so calling memoized(1) again
|
||||
// will create a new cache entry with a new lazy generator
|
||||
memoized(1)
|
||||
// The call count increases because a new lazy value is created and evaluated
|
||||
assert.GreaterOrEqual(t, callCount, 3, "first value should have been evicted")
|
||||
|
||||
// Verify cache still works for remaining values
|
||||
prevCount := callCount
|
||||
memoized(2)
|
||||
memoized(3)
|
||||
// These might or might not increase count depending on eviction
|
||||
assert.GreaterOrEqual(t, callCount, prevCount)
|
||||
})
|
||||
|
||||
t.Run("integrates with key extraction", func(t *testing.T) {
|
||||
type Item struct {
|
||||
ID int
|
||||
Value string
|
||||
}
|
||||
|
||||
// Simple cache
|
||||
simpleCache := func() func(int, func() func() string) func() string {
|
||||
cache := make(map[int]func() string)
|
||||
var mu sync.Mutex
|
||||
|
||||
return func(k int, gen func() func() string) func() string {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if existing, ok := cache[k]; ok {
|
||||
return existing
|
||||
}
|
||||
|
||||
value := gen()
|
||||
cache[k] = value
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
callCount := 0
|
||||
process := func(item Item) string {
|
||||
callCount++
|
||||
return fmt.Sprintf("Processed: %s", item.Value)
|
||||
}
|
||||
|
||||
memoizer := CacheCallback(
|
||||
func(item Item) int { return item.ID },
|
||||
simpleCache(),
|
||||
)
|
||||
|
||||
memoized := memoizer(process)
|
||||
|
||||
item1 := Item{ID: 1, Value: "first"}
|
||||
result1 := memoized(item1)
|
||||
assert.Equal(t, "Processed: first", result1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
// Same ID, different value - should use cache
|
||||
item2 := Item{ID: 1, Value: "second"}
|
||||
result2 := memoized(item2)
|
||||
assert.Equal(t, "Processed: first", result2)
|
||||
assert.Equal(t, 1, callCount)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSingleElementCache tests the SingleElementCache function
|
||||
func TestSingleElementCache(t *testing.T) {
|
||||
f := func(key string) string {
|
||||
return fmt.Sprintf("%s: %d", key, rand.Int())
|
||||
}
|
||||
cb := CacheCallback(func(s string) string { return s }, SingleElementCache[string, string]())
|
||||
cf := cb(f)
|
||||
t.Run("caches single element", func(t *testing.T) {
|
||||
cache := SingleElementCache[int, string]()
|
||||
|
||||
v1 := cf("1")
|
||||
v2 := cf("1")
|
||||
v3 := cf("2")
|
||||
v4 := cf("1")
|
||||
callCount := 0
|
||||
gen := func(n int) func() func() string {
|
||||
// This returns a generator that creates a lazy value
|
||||
return func() func() string {
|
||||
// This is the lazy value that gets cached
|
||||
return func() string {
|
||||
// This gets called when the lazy value is evaluated
|
||||
callCount++
|
||||
return fmt.Sprintf("Value: %d", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, v1, v2)
|
||||
assert.NotEqual(t, v2, v3)
|
||||
assert.NotEqual(t, v3, v4)
|
||||
assert.NotEqual(t, v1, v4)
|
||||
// First call - creates and caches lazy value for key 1
|
||||
lazy1 := cache(1, gen(1))
|
||||
result1 := lazy1()
|
||||
assert.Equal(t, "Value: 1", result1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
// Same key - returns the same cached lazy value
|
||||
lazy1Again := cache(1, gen(1))
|
||||
result2 := lazy1Again()
|
||||
assert.Equal(t, "Value: 1", result2)
|
||||
// The lazy value is called again, so count increases
|
||||
assert.Equal(t, 2, callCount, "cached lazy value is called again")
|
||||
|
||||
// Different key - replaces cache with new lazy value
|
||||
lazy2 := cache(2, gen(2))
|
||||
result3 := lazy2()
|
||||
assert.Equal(t, "Value: 2", result3)
|
||||
assert.Equal(t, 3, callCount)
|
||||
|
||||
// Original key - cache was replaced, creates new lazy value
|
||||
lazy1New := cache(1, gen(1))
|
||||
result4 := lazy1New()
|
||||
assert.Equal(t, "Value: 1", result4)
|
||||
assert.Equal(t, 4, callCount, "new lazy value created after cache replacement")
|
||||
})
|
||||
|
||||
t.Run("works with CacheCallback", func(t *testing.T) {
|
||||
cache := SingleElementCache[int, string]()
|
||||
|
||||
callCount := 0
|
||||
expensive := func(n int) string {
|
||||
callCount++
|
||||
return fmt.Sprintf("Result: %d", n*n)
|
||||
}
|
||||
|
||||
memoizer := CacheCallback(
|
||||
Identity[int],
|
||||
cache,
|
||||
)
|
||||
|
||||
memoized := memoizer(expensive)
|
||||
|
||||
// First computation
|
||||
result1 := memoized(5)
|
||||
assert.Equal(t, "Result: 25", result1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
// Same input - cached
|
||||
result2 := memoized(5)
|
||||
assert.Equal(t, "Result: 25", result2)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
// Different input - replaces cache
|
||||
result3 := memoized(10)
|
||||
assert.Equal(t, "Result: 100", result3)
|
||||
assert.Equal(t, 2, callCount)
|
||||
|
||||
// Back to first input - recomputed
|
||||
result4 := memoized(5)
|
||||
assert.Equal(t, "Result: 25", result4)
|
||||
assert.Equal(t, 3, callCount)
|
||||
})
|
||||
|
||||
t.Run("is thread-safe", func(t *testing.T) {
|
||||
cache := SingleElementCache[int, string]()
|
||||
|
||||
var callCount int32
|
||||
gen := func(n int) func() func() string {
|
||||
return func() func() string {
|
||||
return func() string {
|
||||
atomic.AddInt32(&callCount, 1)
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
return fmt.Sprintf("Value: %d", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
results := make([]string, 20)
|
||||
|
||||
// Concurrent access with same key
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
results[idx] = cache(1, gen(1))()
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Concurrent access with different key
|
||||
for i := 10; i < 20; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
results[idx] = cache(2, gen(2))()
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// All results should be valid (either "Value: 1" or "Value: 2")
|
||||
for _, result := range results {
|
||||
assert.True(t, result == "Value: 1" || result == "Value: 2")
|
||||
}
|
||||
|
||||
// Function should have been called, but exact count depends on race conditions
|
||||
assert.Greater(t, atomic.LoadInt32(&callCount), int32(0))
|
||||
})
|
||||
|
||||
t.Run("handles rapid key changes", func(t *testing.T) {
|
||||
cache := SingleElementCache[int, string]()
|
||||
|
||||
callCount := 0
|
||||
gen := func(n int) func() func() string {
|
||||
return func() func() string {
|
||||
return func() string {
|
||||
callCount++
|
||||
return fmt.Sprintf("Value: %d", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rapidly alternate between keys
|
||||
for i := 0; i < 10; i++ {
|
||||
cache(1, gen(1))()
|
||||
cache(2, gen(2))()
|
||||
}
|
||||
|
||||
// Each key change should trigger a computation
|
||||
// (20 calls total: 10 for key 1, 10 for key 2)
|
||||
assert.Equal(t, 20, callCount)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMemoizeIntegration tests integration scenarios
|
||||
func TestMemoizeIntegration(t *testing.T) {
|
||||
t.Run("fibonacci with memoization", func(t *testing.T) {
|
||||
callCount := 0
|
||||
|
||||
expensive := func(n int) int {
|
||||
callCount++
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
return n * n
|
||||
}
|
||||
|
||||
memoized := Memoize(expensive)
|
||||
|
||||
// First call computes
|
||||
result1 := memoized(10)
|
||||
assert.Equal(t, 100, result1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
// Second call with same input uses cache
|
||||
result2 := memoized(10)
|
||||
assert.Equal(t, 100, result2)
|
||||
assert.Equal(t, 1, callCount, "should use cached value")
|
||||
|
||||
// Different input computes again
|
||||
result3 := memoized(5)
|
||||
assert.Equal(t, 25, result3)
|
||||
assert.Equal(t, 2, callCount)
|
||||
|
||||
// Both values should remain cached
|
||||
assert.Equal(t, 100, memoized(10))
|
||||
assert.Equal(t, 25, memoized(5))
|
||||
assert.Equal(t, 2, callCount, "both values should be cached")
|
||||
})
|
||||
|
||||
t.Run("chaining memoization strategies", func(t *testing.T) {
|
||||
type Request struct {
|
||||
UserID int
|
||||
Action string
|
||||
}
|
||||
|
||||
callCount := 0
|
||||
processRequest := func(r Request) string {
|
||||
callCount++
|
||||
return fmt.Sprintf("User %d: %s", r.UserID, r.Action)
|
||||
}
|
||||
|
||||
// First level: cache by UserID
|
||||
cacheByUser := ContramapMemoize[string](func(r Request) int {
|
||||
return r.UserID
|
||||
})
|
||||
|
||||
memoized := cacheByUser(processRequest)
|
||||
|
||||
req1 := Request{UserID: 1, Action: "login"}
|
||||
result1 := memoized(req1)
|
||||
assert.Equal(t, "User 1: login", result1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
// Same user, different action - uses cache
|
||||
req2 := Request{UserID: 1, Action: "logout"}
|
||||
result2 := memoized(req2)
|
||||
assert.Equal(t, "User 1: login", result2)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
// Different user - computes
|
||||
req3 := Request{UserID: 2, Action: "login"}
|
||||
result3 := memoized(req3)
|
||||
assert.Equal(t, "User 2: login", result3)
|
||||
assert.Equal(t, 2, callCount)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -194,79 +194,6 @@ func TestSecond(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestBind1st tests the Bind1st function
|
||||
func TestBind1st(t *testing.T) {
|
||||
t.Run("binds first parameter of multiplication", func(t *testing.T) {
|
||||
multiply := func(a, b int) int { return a * b }
|
||||
double := Bind1st(multiply, 2)
|
||||
triple := Bind1st(multiply, 3)
|
||||
|
||||
assert.Equal(t, 10, double(5))
|
||||
assert.Equal(t, 20, double(10))
|
||||
assert.Equal(t, 15, triple(5))
|
||||
})
|
||||
|
||||
t.Run("binds first parameter of division", func(t *testing.T) {
|
||||
divide := func(a, b float64) float64 { return a / b }
|
||||
divideBy10 := Bind1st(divide, 10.0)
|
||||
|
||||
assert.Equal(t, 5.0, divideBy10(2.0))
|
||||
assert.Equal(t, 2.0, divideBy10(5.0))
|
||||
})
|
||||
|
||||
t.Run("binds first parameter of string concatenation", func(t *testing.T) {
|
||||
concat := func(a, b string) string { return a + b }
|
||||
addHello := Bind1st(concat, "Hello ")
|
||||
|
||||
assert.Equal(t, "Hello World", addHello("World"))
|
||||
assert.Equal(t, "Hello Go", addHello("Go"))
|
||||
})
|
||||
}
|
||||
|
||||
// TestBind2nd tests the Bind2nd function
|
||||
func TestBind2nd(t *testing.T) {
|
||||
t.Run("binds second parameter of multiplication", func(t *testing.T) {
|
||||
multiply := func(a, b int) int { return a * b }
|
||||
double := Bind2nd(multiply, 2)
|
||||
triple := Bind2nd(multiply, 3)
|
||||
|
||||
assert.Equal(t, 10, double(5))
|
||||
assert.Equal(t, 20, double(10))
|
||||
assert.Equal(t, 15, triple(5))
|
||||
})
|
||||
|
||||
t.Run("binds second parameter of division", func(t *testing.T) {
|
||||
divide := func(a, b float64) float64 { return a / b }
|
||||
halve := Bind2nd(divide, 2.0)
|
||||
|
||||
assert.Equal(t, 5.0, halve(10.0))
|
||||
assert.Equal(t, 2.5, halve(5.0))
|
||||
})
|
||||
|
||||
t.Run("binds second parameter of subtraction", func(t *testing.T) {
|
||||
subtract := func(a, b int) int { return a - b }
|
||||
decrementBy5 := Bind2nd(subtract, 5)
|
||||
|
||||
assert.Equal(t, 5, decrementBy5(10))
|
||||
assert.Equal(t, 0, decrementBy5(5))
|
||||
})
|
||||
}
|
||||
|
||||
// TestSK tests the SK function
|
||||
func TestSK(t *testing.T) {
|
||||
t.Run("returns second argument ignoring first", func(t *testing.T) {
|
||||
assert.Equal(t, "hello", SK(42, "hello"))
|
||||
assert.Equal(t, 100, SK(true, 100))
|
||||
assert.Equal(t, 3.14, SK("test", 3.14))
|
||||
})
|
||||
|
||||
t.Run("behaves like Second", func(t *testing.T) {
|
||||
// SK should be identical to Second
|
||||
assert.Equal(t, Second(42, "hello"), SK(42, "hello"))
|
||||
assert.Equal(t, Second(true, 100), SK(true, 100))
|
||||
})
|
||||
}
|
||||
|
||||
// TestTernary tests the Ternary function
|
||||
func TestTernary(t *testing.T) {
|
||||
t.Run("applies onTrue when predicate is true", func(t *testing.T) {
|
||||
|
||||
@@ -13,11 +13,45 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package content provides constants for common HTTP Content-Type header values.
|
||||
//
|
||||
// These constants can be used when setting or checking Content-Type headers in HTTP
|
||||
// requests and responses, ensuring consistency and avoiding typos in content type strings.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// req.Header.Set("Content-Type", content.JSON)
|
||||
// if contentType == content.TextPlain {
|
||||
// // handle plain text
|
||||
// }
|
||||
package content
|
||||
|
||||
const (
|
||||
TextPlain = "text/plain"
|
||||
JSON = "application/json"
|
||||
Json = JSON // Deprecated: use [JSON] instead
|
||||
// TextPlain represents the "text/plain" content type for plain text data.
|
||||
// This is commonly used for simple text responses or requests without any
|
||||
// specific formatting or structure.
|
||||
//
|
||||
// Defined in RFC 2046, Section 4.1.3: https://www.rfc-editor.org/rfc/rfc2046.html#section-4.1.3
|
||||
TextPlain = "text/plain"
|
||||
|
||||
// JSON represents the "application/json" content type for JSON-encoded data.
|
||||
// This is the standard content type for JSON payloads in HTTP requests and responses.
|
||||
//
|
||||
// Defined in RFC 8259: https://www.rfc-editor.org/rfc/rfc8259.html
|
||||
JSON = "application/json"
|
||||
|
||||
// Json is deprecated. Use [JSON] instead.
|
||||
//
|
||||
// Deprecated: Use JSON for consistency with Go naming conventions.
|
||||
Json = JSON
|
||||
|
||||
// FormEncoded represents the "application/x-www-form-urlencoded" content type.
|
||||
// This is used for HTML form submissions where form data is encoded as key-value
|
||||
// pairs in the request body, with keys and values URL-encoded.
|
||||
//
|
||||
// Defined in HTML 4.01 Specification, Section 17.13.4:
|
||||
// https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4
|
||||
// Also referenced in WHATWG HTML Living Standard:
|
||||
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#application/x-www-form-urlencoded-encoding-algorithm
|
||||
FormEncoded = "application/x-www-form-urlencoded"
|
||||
)
|
||||
|
||||
@@ -13,6 +13,62 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package form provides functional utilities for working with HTTP form data (url.Values).
|
||||
//
|
||||
// This package offers a functional approach to building and manipulating HTTP form data
|
||||
// using lenses, endomorphisms, and monoids. It enables immutable transformations of
|
||||
// url.Values through composable operations.
|
||||
//
|
||||
// # Core Concepts
|
||||
//
|
||||
// The package is built around several key abstractions:
|
||||
// - Endomorphism: A function that transforms url.Values immutably
|
||||
// - Lenses: Optics for focusing on specific form fields
|
||||
// - Monoids: For combining form transformations and values
|
||||
//
|
||||
// # Basic Usage
|
||||
//
|
||||
// Create form data by composing endomorphisms:
|
||||
//
|
||||
// form := F.Pipe3(
|
||||
// form.Default,
|
||||
// form.WithValue("username")("john"),
|
||||
// form.WithValue("email")("john@example.com"),
|
||||
// form.WithValue("age")("30"),
|
||||
// )
|
||||
//
|
||||
// Remove fields from forms:
|
||||
//
|
||||
// updated := F.Pipe1(
|
||||
// form,
|
||||
// form.WithoutValue("age"),
|
||||
// )
|
||||
//
|
||||
// # Lenses
|
||||
//
|
||||
// The package provides two main lenses:
|
||||
// - AtValues: Focuses on all values of a form field ([]string)
|
||||
// - AtValue: Focuses on the first value of a form field (Option[string])
|
||||
//
|
||||
// Use lenses to read and update form fields:
|
||||
//
|
||||
// lens := form.AtValue("username")
|
||||
// value := lens.Get(form) // Returns Option[string]
|
||||
// updated := lens.Set(O.Some("jane"))(form)
|
||||
//
|
||||
// # Monoids
|
||||
//
|
||||
// Combine multiple form transformations:
|
||||
//
|
||||
// transform := form.Monoid.Concat(
|
||||
// form.WithValue("field1")("value1"),
|
||||
// form.WithValue("field2")("value2"),
|
||||
// )
|
||||
// result := transform(form.Default)
|
||||
//
|
||||
// Merge form values:
|
||||
//
|
||||
// merged := form.ValuesMonoid.Concat(form1, form2)
|
||||
package form
|
||||
|
||||
import (
|
||||
@@ -29,23 +85,61 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
// Endomorphism returns an [ENDO.Endomorphism] that transforms a form
|
||||
// Endomorphism is a function that transforms url.Values immutably.
|
||||
// It represents a transformation from url.Values to url.Values,
|
||||
// enabling functional composition of form modifications.
|
||||
//
|
||||
// Example:
|
||||
// transform := form.WithValue("key")("value")
|
||||
// result := transform(form.Default)
|
||||
Endomorphism = ENDO.Endomorphism[url.Values]
|
||||
)
|
||||
|
||||
var (
|
||||
// Default is the default form field
|
||||
// Default is an empty url.Values that serves as the starting point
|
||||
// for building form data. Use this with Pipe operations to construct
|
||||
// forms functionally.
|
||||
//
|
||||
// Example:
|
||||
// form := F.Pipe2(
|
||||
// form.Default,
|
||||
// form.WithValue("key1")("value1"),
|
||||
// form.WithValue("key2")("value2"),
|
||||
// )
|
||||
Default = make(url.Values)
|
||||
|
||||
noField = O.None[string]()
|
||||
|
||||
// Monoid is the [M.Monoid] for the [Endomorphism]
|
||||
// Monoid is a Monoid for Endomorphism that allows combining multiple
|
||||
// form transformations into a single transformation. The identity element
|
||||
// is the identity function, and concatenation composes transformations.
|
||||
//
|
||||
// Example:
|
||||
// transform := form.Monoid.Concat(
|
||||
// form.WithValue("field1")("value1"),
|
||||
// form.WithValue("field2")("value2"),
|
||||
// )
|
||||
// result := transform(form.Default)
|
||||
Monoid = ENDO.Monoid[url.Values]()
|
||||
|
||||
// ValuesMonoid is a [M.Monoid] to concatenate [url.Values] maps
|
||||
// ValuesMonoid is a Monoid for url.Values that concatenates form data.
|
||||
// When two forms are combined, arrays of values for the same key are
|
||||
// concatenated using the array Semigroup.
|
||||
//
|
||||
// Example:
|
||||
// form1 := url.Values{"key": []string{"value1"}}
|
||||
// form2 := url.Values{"key": []string{"value2"}}
|
||||
// merged := form.ValuesMonoid.Concat(form1, form2)
|
||||
// // Result: url.Values{"key": []string{"value1", "value2"}}
|
||||
ValuesMonoid = RG.UnionMonoid[url.Values](A.Semigroup[string]())
|
||||
|
||||
// AtValues is a [L.Lens] that focusses on the values of a form field
|
||||
// AtValues is a Lens that focuses on all values of a form field as a slice.
|
||||
// It provides access to the complete []string array for a given field name.
|
||||
//
|
||||
// Example:
|
||||
// lens := form.AtValues("tags")
|
||||
// values := lens.Get(form) // Returns Option[[]string]
|
||||
// updated := lens.Set(O.Some([]string{"tag1", "tag2"}))(form)
|
||||
AtValues = LRG.AtRecord[url.Values, []string]
|
||||
|
||||
composeHead = F.Pipe1(
|
||||
@@ -53,14 +147,39 @@ var (
|
||||
LO.Compose[url.Values, string](A.Empty[string]()),
|
||||
)
|
||||
|
||||
// AtValue is a [L.Lens] that focusses on first value in form fields
|
||||
// AtValue is a Lens that focuses on the first value of a form field.
|
||||
// It returns an Option[string] representing the first value if present,
|
||||
// or None if the field doesn't exist or has no values.
|
||||
//
|
||||
// Example:
|
||||
// lens := form.AtValue("username")
|
||||
// value := lens.Get(form) // Returns Option[string]
|
||||
// updated := lens.Set(O.Some("newuser"))(form)
|
||||
AtValue = F.Flow2(
|
||||
AtValues,
|
||||
composeHead,
|
||||
)
|
||||
)
|
||||
|
||||
// WithValue creates a [FormBuilder] for a certain field
|
||||
// WithValue creates an Endomorphism that sets a form field to a specific value.
|
||||
// It returns a curried function that takes the field name first, then the value,
|
||||
// and finally returns a transformation function.
|
||||
//
|
||||
// The transformation is immutable - it creates a new url.Values rather than
|
||||
// modifying the input.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Set a single field
|
||||
// form := form.WithValue("username")("john")(form.Default)
|
||||
//
|
||||
// // Compose multiple fields
|
||||
// form := F.Pipe3(
|
||||
// form.Default,
|
||||
// form.WithValue("username")("john"),
|
||||
// form.WithValue("email")("john@example.com"),
|
||||
// form.WithValue("age")("30"),
|
||||
// )
|
||||
func WithValue(name string) func(value string) Endomorphism {
|
||||
return F.Flow2(
|
||||
O.Of[string],
|
||||
@@ -68,7 +187,21 @@ func WithValue(name string) func(value string) Endomorphism {
|
||||
)
|
||||
}
|
||||
|
||||
// WithoutValue creates a [FormBuilder] that removes a field
|
||||
// WithoutValue creates an Endomorphism that removes a form field.
|
||||
// The transformation is immutable - it creates a new url.Values rather than
|
||||
// modifying the input.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Remove a field
|
||||
// updated := form.WithoutValue("age")(form)
|
||||
//
|
||||
// // Compose with other operations
|
||||
// form := F.Pipe2(
|
||||
// existingForm,
|
||||
// form.WithValue("username")("john"),
|
||||
// form.WithoutValue("password"),
|
||||
// )
|
||||
func WithoutValue(name string) Endomorphism {
|
||||
return AtValue(name).Set(noField)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package form
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
@@ -91,3 +92,448 @@ func TestFormField(t *testing.T) {
|
||||
assert.Equal(t, O.Of("v1"), l1.Get(v2))
|
||||
assert.Equal(t, O.Of("v2"), l2.Get(v2))
|
||||
}
|
||||
|
||||
// TestWithValue tests the WithValue function
|
||||
func TestWithValue(t *testing.T) {
|
||||
t.Run("sets a single value", func(t *testing.T) {
|
||||
form := WithValue("key")("value")(Default)
|
||||
assert.Equal(t, "value", form.Get("key"))
|
||||
})
|
||||
|
||||
t.Run("creates immutable transformation", func(t *testing.T) {
|
||||
original := Default
|
||||
modified := WithValue("key")("value")(original)
|
||||
|
||||
assert.False(t, valuesEq.Equals(original, modified))
|
||||
assert.Equal(t, "", original.Get("key"))
|
||||
assert.Equal(t, "value", modified.Get("key"))
|
||||
})
|
||||
|
||||
t.Run("overwrites existing value", func(t *testing.T) {
|
||||
form := WithValue("key")("value1")(Default)
|
||||
updated := WithValue("key")("value2")(form)
|
||||
|
||||
assert.Equal(t, "value2", updated.Get("key"))
|
||||
assert.Equal(t, "value1", form.Get("key"))
|
||||
})
|
||||
|
||||
t.Run("composes multiple values", func(t *testing.T) {
|
||||
form := F.Pipe3(
|
||||
Default,
|
||||
WithValue("key1")("value1"),
|
||||
WithValue("key2")("value2"),
|
||||
WithValue("key3")("value3"),
|
||||
)
|
||||
|
||||
assert.Equal(t, "value1", form.Get("key1"))
|
||||
assert.Equal(t, "value2", form.Get("key2"))
|
||||
assert.Equal(t, "value3", form.Get("key3"))
|
||||
})
|
||||
|
||||
t.Run("handles empty string values", func(t *testing.T) {
|
||||
form := WithValue("key")("")(Default)
|
||||
assert.Equal(t, "", form.Get("key"))
|
||||
assert.True(t, form.Has("key"))
|
||||
})
|
||||
|
||||
t.Run("handles special characters in keys", func(t *testing.T) {
|
||||
form := F.Pipe2(
|
||||
Default,
|
||||
WithValue("key-with-dash")("value1"),
|
||||
WithValue("key_with_underscore")("value2"),
|
||||
)
|
||||
|
||||
assert.Equal(t, "value1", form.Get("key-with-dash"))
|
||||
assert.Equal(t, "value2", form.Get("key_with_underscore"))
|
||||
})
|
||||
}
|
||||
|
||||
// TestWithoutValue tests the WithoutValue function
|
||||
func TestWithoutValue(t *testing.T) {
|
||||
t.Run("clears field value", func(t *testing.T) {
|
||||
form := WithValue("key")("value")(Default)
|
||||
updated := WithoutValue("key")(form)
|
||||
|
||||
// WithoutValue sets the field to an empty array, not removing it entirely
|
||||
assert.Equal(t, "", updated.Get("key"))
|
||||
// The field still exists but with empty values
|
||||
values := updated["key"]
|
||||
assert.Equal(t, 0, len(values))
|
||||
})
|
||||
|
||||
t.Run("is idempotent", func(t *testing.T) {
|
||||
form := WithValue("key")("value")(Default)
|
||||
removed1 := WithoutValue("key")(form)
|
||||
removed2 := WithoutValue("key")(removed1)
|
||||
|
||||
assert.True(t, valuesEq.Equals(removed1, removed2))
|
||||
})
|
||||
|
||||
t.Run("does not affect other fields", func(t *testing.T) {
|
||||
form := F.Pipe2(
|
||||
Default,
|
||||
WithValue("key1")("value1"),
|
||||
WithValue("key2")("value2"),
|
||||
)
|
||||
updated := WithoutValue("key1")(form)
|
||||
|
||||
assert.Equal(t, "", updated.Get("key1"))
|
||||
assert.Equal(t, "value2", updated.Get("key2"))
|
||||
})
|
||||
|
||||
t.Run("creates immutable transformation", func(t *testing.T) {
|
||||
form := WithValue("key")("value")(Default)
|
||||
updated := WithoutValue("key")(form)
|
||||
|
||||
assert.False(t, valuesEq.Equals(form, updated))
|
||||
assert.Equal(t, "value", form.Get("key"))
|
||||
assert.Equal(t, "", updated.Get("key"))
|
||||
})
|
||||
|
||||
t.Run("handles non-existent field", func(t *testing.T) {
|
||||
form := Default
|
||||
updated := WithoutValue("nonexistent")(form)
|
||||
|
||||
assert.True(t, valuesEq.Equals(form, updated))
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonoid tests the Monoid for Endomorphism
|
||||
func TestMonoid(t *testing.T) {
|
||||
t.Run("identity element", func(t *testing.T) {
|
||||
form := F.Pipe1(
|
||||
Default,
|
||||
WithValue("key")("value"),
|
||||
)
|
||||
|
||||
// Concatenating with identity should not change the result
|
||||
result := Monoid.Concat(Monoid.Empty(), WithValue("key")("value"))(Default)
|
||||
assert.True(t, valuesEq.Equals(form, result))
|
||||
})
|
||||
|
||||
t.Run("concatenates transformations", func(t *testing.T) {
|
||||
transform := Monoid.Concat(
|
||||
WithValue("key1")("value1"),
|
||||
WithValue("key2")("value2"),
|
||||
)
|
||||
result := transform(Default)
|
||||
|
||||
assert.Equal(t, "value1", result.Get("key1"))
|
||||
assert.Equal(t, "value2", result.Get("key2"))
|
||||
})
|
||||
|
||||
t.Run("concatenates multiple transformations", func(t *testing.T) {
|
||||
transform := Monoid.Concat(
|
||||
WithValue("key1")("value1"),
|
||||
Monoid.Concat(
|
||||
WithValue("key2")("value2"),
|
||||
WithValue("key3")("value3"),
|
||||
),
|
||||
)
|
||||
result := transform(Default)
|
||||
|
||||
assert.Equal(t, "value1", result.Get("key1"))
|
||||
assert.Equal(t, "value2", result.Get("key2"))
|
||||
assert.Equal(t, "value3", result.Get("key3"))
|
||||
})
|
||||
|
||||
t.Run("respects transformation order", func(t *testing.T) {
|
||||
// Monoid concatenation composes functions left-to-right
|
||||
// So the first transformation is applied first, then the second
|
||||
transform := Monoid.Concat(
|
||||
WithValue("key")("first"),
|
||||
WithValue("key")("second"),
|
||||
)
|
||||
result := transform(Default)
|
||||
|
||||
// The transformations are composed, so first is applied, then second overwrites it
|
||||
// But since Monoid.Concat composes endomorphisms, we need to check actual behavior
|
||||
assert.Equal(t, "first", result.Get("key"))
|
||||
})
|
||||
}
|
||||
|
||||
// TestValuesMonoid tests the ValuesMonoid
|
||||
func TestValuesMonoid(t *testing.T) {
|
||||
t.Run("identity element", func(t *testing.T) {
|
||||
form := url.Values{"key": []string{"value"}}
|
||||
result := ValuesMonoid.Concat(ValuesMonoid.Empty(), form)
|
||||
|
||||
assert.True(t, valuesEq.Equals(form, result))
|
||||
})
|
||||
|
||||
t.Run("concatenates disjoint forms", func(t *testing.T) {
|
||||
form1 := url.Values{"key1": []string{"value1"}}
|
||||
form2 := url.Values{"key2": []string{"value2"}}
|
||||
result := ValuesMonoid.Concat(form1, form2)
|
||||
|
||||
assert.Equal(t, "value1", result.Get("key1"))
|
||||
assert.Equal(t, "value2", result.Get("key2"))
|
||||
})
|
||||
|
||||
t.Run("concatenates arrays for same key", func(t *testing.T) {
|
||||
form1 := url.Values{"key": []string{"value1"}}
|
||||
form2 := url.Values{"key": []string{"value2"}}
|
||||
result := ValuesMonoid.Concat(form1, form2)
|
||||
|
||||
values := result["key"]
|
||||
assert.Equal(t, 2, len(values))
|
||||
assert.Equal(t, "value1", values[0])
|
||||
assert.Equal(t, "value2", values[1])
|
||||
})
|
||||
|
||||
t.Run("is associative", func(t *testing.T) {
|
||||
form1 := url.Values{"key": []string{"value1"}}
|
||||
form2 := url.Values{"key": []string{"value2"}}
|
||||
form3 := url.Values{"key": []string{"value3"}}
|
||||
|
||||
result1 := ValuesMonoid.Concat(ValuesMonoid.Concat(form1, form2), form3)
|
||||
result2 := ValuesMonoid.Concat(form1, ValuesMonoid.Concat(form2, form3))
|
||||
|
||||
assert.True(t, valuesEq.Equals(result1, result2))
|
||||
})
|
||||
}
|
||||
|
||||
// TestAtValues tests the AtValues lens
|
||||
func TestAtValues(t *testing.T) {
|
||||
t.Run("gets values array", func(t *testing.T) {
|
||||
form := url.Values{"key": []string{"value1", "value2"}}
|
||||
lens := AtValues("key")
|
||||
|
||||
result := lens.Get(form)
|
||||
assert.True(t, O.IsSome(result))
|
||||
values := O.GetOrElse(F.Constant([]string{}))(result)
|
||||
assert.Equal(t, 2, len(values))
|
||||
assert.Equal(t, "value1", values[0])
|
||||
assert.Equal(t, "value2", values[1])
|
||||
})
|
||||
|
||||
t.Run("returns None for non-existent key", func(t *testing.T) {
|
||||
lens := AtValues("nonexistent")
|
||||
result := lens.Get(Default)
|
||||
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("sets values array", func(t *testing.T) {
|
||||
lens := AtValues("key")
|
||||
form := lens.Set(O.Some([]string{"value1", "value2"}))(Default)
|
||||
|
||||
values := form["key"]
|
||||
assert.Equal(t, 2, len(values))
|
||||
assert.Equal(t, "value1", values[0])
|
||||
assert.Equal(t, "value2", values[1])
|
||||
})
|
||||
|
||||
t.Run("removes field with None", func(t *testing.T) {
|
||||
form := url.Values{"key": []string{"value"}}
|
||||
lens := AtValues("key")
|
||||
updated := lens.Set(O.None[[]string]())(form)
|
||||
|
||||
assert.False(t, updated.Has("key"))
|
||||
})
|
||||
|
||||
t.Run("creates immutable transformation", func(t *testing.T) {
|
||||
form := url.Values{"key": []string{"value1"}}
|
||||
lens := AtValues("key")
|
||||
updated := lens.Set(O.Some([]string{"value2"}))(form)
|
||||
|
||||
assert.False(t, valuesEq.Equals(form, updated))
|
||||
assert.Equal(t, "value1", form.Get("key"))
|
||||
assert.Equal(t, "value2", updated.Get("key"))
|
||||
})
|
||||
}
|
||||
|
||||
// TestAtValue tests the AtValue lens
|
||||
func TestAtValue(t *testing.T) {
|
||||
t.Run("gets first value", func(t *testing.T) {
|
||||
form := url.Values{"key": []string{"value1", "value2"}}
|
||||
lens := AtValue("key")
|
||||
|
||||
result := lens.Get(form)
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, "value1", O.GetOrElse(F.Constant(""))(result))
|
||||
})
|
||||
|
||||
t.Run("returns None for non-existent key", func(t *testing.T) {
|
||||
lens := AtValue("nonexistent")
|
||||
result := lens.Get(Default)
|
||||
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("returns None for empty array", func(t *testing.T) {
|
||||
form := url.Values{"key": []string{}}
|
||||
lens := AtValue("key")
|
||||
result := lens.Get(form)
|
||||
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("sets first value", func(t *testing.T) {
|
||||
lens := AtValue("key")
|
||||
form := lens.Set(O.Some("value"))(Default)
|
||||
|
||||
assert.Equal(t, "value", form.Get("key"))
|
||||
})
|
||||
|
||||
t.Run("replaces first value in array", func(t *testing.T) {
|
||||
form := url.Values{"key": []string{"old1", "old2"}}
|
||||
lens := AtValue("key")
|
||||
updated := lens.Set(O.Some("new"))(form)
|
||||
|
||||
values := updated["key"]
|
||||
// AtValue modifies the head of the array, keeping other elements
|
||||
assert.Equal(t, 2, len(values))
|
||||
assert.Equal(t, "new", values[0])
|
||||
assert.Equal(t, "old2", values[1])
|
||||
})
|
||||
|
||||
t.Run("clears field with None", func(t *testing.T) {
|
||||
form := url.Values{"key": []string{"value"}}
|
||||
lens := AtValue("key")
|
||||
updated := lens.Set(O.None[string]())(form)
|
||||
|
||||
// Setting to None creates an empty array, not removing the key
|
||||
values := updated["key"]
|
||||
assert.Equal(t, 0, len(values))
|
||||
})
|
||||
}
|
||||
|
||||
// Example tests demonstrating package usage
|
||||
|
||||
// ExampleWithValue demonstrates how to set form field values
|
||||
func ExampleWithValue() {
|
||||
// Create a form with a single field
|
||||
form := WithValue("username")("john")(Default)
|
||||
fmt.Println(form.Get("username"))
|
||||
// Output: john
|
||||
}
|
||||
|
||||
// ExampleWithValue_composition demonstrates composing multiple field assignments
|
||||
func ExampleWithValue_composition() {
|
||||
// Build a form with multiple fields using Pipe
|
||||
form := F.Pipe3(
|
||||
Default,
|
||||
WithValue("username")("john"),
|
||||
WithValue("email")("john@example.com"),
|
||||
WithValue("age")("30"),
|
||||
)
|
||||
|
||||
fmt.Println(form.Get("username"))
|
||||
fmt.Println(form.Get("email"))
|
||||
fmt.Println(form.Get("age"))
|
||||
// Output:
|
||||
// john
|
||||
// john@example.com
|
||||
// 30
|
||||
}
|
||||
|
||||
// ExampleWithoutValue demonstrates clearing a form field value
|
||||
func ExampleWithoutValue() {
|
||||
// Create a form and then clear a field
|
||||
form := F.Pipe2(
|
||||
Default,
|
||||
WithValue("username")("john"),
|
||||
WithValue("password")("secret"),
|
||||
)
|
||||
|
||||
// Clear the password field (sets it to empty array)
|
||||
sanitized := WithoutValue("password")(form)
|
||||
|
||||
fmt.Println(sanitized.Get("username"))
|
||||
fmt.Println(sanitized.Get("password"))
|
||||
// Output:
|
||||
// john
|
||||
//
|
||||
}
|
||||
|
||||
// ExampleAtValue demonstrates using the AtValue lens
|
||||
func ExampleAtValue() {
|
||||
form := WithValue("username")("john")(Default)
|
||||
|
||||
// Get a value using the lens
|
||||
lens := AtValue("username")
|
||||
value := lens.Get(form)
|
||||
|
||||
fmt.Println(O.IsSome(value))
|
||||
fmt.Println(O.GetOrElse(F.Constant("default"))(value))
|
||||
// Output:
|
||||
// true
|
||||
// john
|
||||
}
|
||||
|
||||
// ExampleAtValue_set demonstrates setting a value using the AtValue lens
|
||||
func ExampleAtValue_set() {
|
||||
form := WithValue("username")("john")(Default)
|
||||
|
||||
// Update the value using the lens
|
||||
lens := AtValue("username")
|
||||
updated := lens.Set(O.Some("jane"))(form)
|
||||
|
||||
fmt.Println(updated.Get("username"))
|
||||
// Output: jane
|
||||
}
|
||||
|
||||
// ExampleMonoid demonstrates combining form transformations
|
||||
func ExampleMonoid() {
|
||||
// Combine multiple transformations into one
|
||||
transform := Monoid.Concat(
|
||||
WithValue("field1")("value1"),
|
||||
WithValue("field2")("value2"),
|
||||
)
|
||||
|
||||
result := transform(Default)
|
||||
fmt.Println(result.Get("field1"))
|
||||
fmt.Println(result.Get("field2"))
|
||||
// Output:
|
||||
// value1
|
||||
// value2
|
||||
}
|
||||
|
||||
// ExampleValuesMonoid demonstrates merging form data
|
||||
func ExampleValuesMonoid() {
|
||||
form1 := url.Values{"key1": []string{"value1"}}
|
||||
form2 := url.Values{"key2": []string{"value2"}}
|
||||
|
||||
merged := ValuesMonoid.Concat(form1, form2)
|
||||
|
||||
fmt.Println(merged.Get("key1"))
|
||||
fmt.Println(merged.Get("key2"))
|
||||
// Output:
|
||||
// value1
|
||||
// value2
|
||||
}
|
||||
|
||||
// ExampleValuesMonoid_concatenation demonstrates array concatenation for same keys
|
||||
func ExampleValuesMonoid_concatenation() {
|
||||
form1 := url.Values{"tags": []string{"go"}}
|
||||
form2 := url.Values{"tags": []string{"functional"}}
|
||||
|
||||
merged := ValuesMonoid.Concat(form1, form2)
|
||||
|
||||
tags := merged["tags"]
|
||||
fmt.Println(len(tags))
|
||||
fmt.Println(tags[0])
|
||||
fmt.Println(tags[1])
|
||||
// Output:
|
||||
// 2
|
||||
// go
|
||||
// functional
|
||||
}
|
||||
|
||||
// ExampleAtValues demonstrates working with multiple values
|
||||
func ExampleAtValues() {
|
||||
form := url.Values{"tags": []string{"go", "functional", "programming"}}
|
||||
|
||||
lens := AtValues("tags")
|
||||
values := lens.Get(form)
|
||||
|
||||
if O.IsSome(values) {
|
||||
tags := O.GetOrElse(F.Constant([]string{}))(values)
|
||||
fmt.Println(len(tags))
|
||||
fmt.Println(tags[0])
|
||||
}
|
||||
// Output:
|
||||
// 3
|
||||
// go
|
||||
}
|
||||
|
||||
@@ -917,11 +917,134 @@ func Local[A, R1, R2 any](f func(R2) R1) func(ReaderIOResult[R1, A]) ReaderIORes
|
||||
return reader.Local[IOResult[A]](f)
|
||||
}
|
||||
|
||||
// Read executes a ReaderIOResult by providing it with a concrete environment value.
|
||||
// This function "runs" the reader computation by supplying the required environment,
|
||||
// converting a ReaderIOResult into an IOResult that can be executed.
|
||||
//
|
||||
// This is the fundamental way to execute a ReaderIOResult computation - you provide
|
||||
// the environment it needs, and get back an IOResult that can be run.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the success value
|
||||
// - R: The type of the environment/context
|
||||
//
|
||||
// Parameters:
|
||||
// - r: The environment value to provide to the computation
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderIOResult and returns an IOResult
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Define a computation that needs configuration
|
||||
// computation := func(cfg Config) IOResult[string] {
|
||||
// return func() (string, error) {
|
||||
// return fmt.Sprintf("Value: %d", cfg.Value), nil
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Provide the configuration and execute
|
||||
// cfg := Config{Value: 42}
|
||||
// result := Read[string](cfg)(computation)
|
||||
// value, err := result() // Returns "Value: 42", nil
|
||||
//
|
||||
//go:inline
|
||||
func Read[A, R any](r R) func(ReaderIOResult[R, A]) IOResult[A] {
|
||||
return reader.Read[IOResult[A]](r)
|
||||
}
|
||||
|
||||
// ReadIO executes a ReaderIOResult by providing it with an environment value wrapped in IO.
|
||||
// This is useful when the environment itself needs to be computed or retrieved through an IO operation.
|
||||
// The IO effect is executed first to obtain the environment, then that environment is provided
|
||||
// to the ReaderIOResult computation.
|
||||
//
|
||||
// This allows for dynamic environment resolution where the configuration or context is not
|
||||
// immediately available but must be computed or fetched.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the success value
|
||||
// - R: The type of the environment/context
|
||||
//
|
||||
// Parameters:
|
||||
// - r: An IO operation that produces the environment value
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderIOResult and returns an IOResult
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Environment that needs to be loaded
|
||||
// loadConfig := func() Config {
|
||||
// // Simulate loading config from file or environment
|
||||
// return Config{Value: 42}
|
||||
// }
|
||||
//
|
||||
// // Computation that needs the config
|
||||
// computation := func(cfg Config) IOResult[string] {
|
||||
// return func() (string, error) {
|
||||
// return fmt.Sprintf("Loaded: %d", cfg.Value), nil
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Load config and execute computation
|
||||
// result := ReadIO[string](loadConfig)(computation)
|
||||
// value, err := result() // Loads config, then returns "Loaded: 42", nil
|
||||
func ReadIO[A, R any](r IO[R]) func(ReaderIOResult[R, A]) IOResult[A] {
|
||||
return func(ri ReaderIOResult[R, A]) IOResult[A] {
|
||||
return func() (A, error) {
|
||||
return ri(r())()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReadIOResult executes a ReaderIOResult by providing it with an environment value wrapped in IOResult.
|
||||
// This is the most flexible variant, allowing the environment itself to be the result of a computation
|
||||
// that may fail. If the environment computation fails, the entire computation fails without executing
|
||||
// the ReaderIOResult.
|
||||
//
|
||||
// This is useful when the environment must be validated, loaded from external sources, or computed
|
||||
// in a way that might fail. The error from environment resolution is propagated as the final error.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the success value
|
||||
// - R: The type of the environment/context
|
||||
//
|
||||
// Parameters:
|
||||
// - r: An IOResult operation that produces the environment value or an error
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderIOResult and returns an IOResult
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Environment that might fail to load
|
||||
// loadConfig := func() (Config, error) {
|
||||
// cfg, err := os.ReadFile("config.json")
|
||||
// if err != nil {
|
||||
// return Config{}, fmt.Errorf("failed to load config: %w", err)
|
||||
// }
|
||||
// return parseConfig(cfg)
|
||||
// }
|
||||
//
|
||||
// // Computation that needs the config
|
||||
// computation := func(cfg Config) IOResult[string] {
|
||||
// return func() (string, error) {
|
||||
// return fmt.Sprintf("Using: %d", cfg.Value), nil
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Try to load config and execute computation
|
||||
// result := ReadIOResult[string](loadConfig)(computation)
|
||||
// value, err := result() // Returns error if config loading fails
|
||||
//
|
||||
//go:inline
|
||||
func ReadIOResult[A, R any](r IOResult[R]) func(ReaderIOResult[R, A]) IOResult[A] {
|
||||
return function.Flow2(
|
||||
ioresult.Chain[R, A],
|
||||
Read[A](r),
|
||||
)
|
||||
}
|
||||
|
||||
// //go:inline
|
||||
// func MonadChainLeft[RAB, A any](fa ReaderIOResult[RA, A], f Kleisli[RBA, A]) ReaderIOResult[RB, A] {
|
||||
// return readert.MonadChain(
|
||||
|
||||
535
v2/idiomatic/readerioresult/reader_bench_test.go
Normal file
535
v2/idiomatic/readerioresult/reader_bench_test.go
Normal file
@@ -0,0 +1,535 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
IOR "github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
type benchConfig struct {
|
||||
value int
|
||||
}
|
||||
|
||||
var (
|
||||
benchErr = errors.New("benchmark error")
|
||||
benchCfg = benchConfig{value: 100}
|
||||
benchResult Result[int]
|
||||
benchRIOE ReaderIOResult[benchConfig, int]
|
||||
benchInt int
|
||||
)
|
||||
|
||||
// Benchmark core constructors
|
||||
func BenchmarkLeft(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = Left[benchConfig, int](benchErr)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRight(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = Right[benchConfig](42)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkOf(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = Of[benchConfig](42)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFromEither_Right(b *testing.B) {
|
||||
either := E.Right[error](42)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = FromEither[benchConfig](either)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFromEither_Left(b *testing.B) {
|
||||
either := E.Left[int](benchErr)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = FromEither[benchConfig](either)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFromIO(b *testing.B) {
|
||||
io := func() int { return 42 }
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = FromIO[benchConfig, error](io)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFromIOResult_Right(b *testing.B) {
|
||||
ioe := IOR.Of(42)
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = FromIOResult[benchConfig](ioe)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFromIOResult_Left(b *testing.B) {
|
||||
ioe := IOR.Left[int](benchErr)
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = FromIOResult[benchConfig](ioe)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark execution
|
||||
func BenchmarkExecute_Right(b *testing.B) {
|
||||
rioe := Right[benchConfig](42)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
val, err := rioe(benchCfg)()
|
||||
if err != nil {
|
||||
benchResult = E.Left[int](err)
|
||||
} else {
|
||||
benchResult = E.Right[error](val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExecute_Left(b *testing.B) {
|
||||
rioe := Left[benchConfig, int](benchErr)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
val, err := rioe(benchCfg)()
|
||||
if err != nil {
|
||||
benchResult = E.Left[int](err)
|
||||
} else {
|
||||
benchResult = E.Right[error](val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark functor operations
|
||||
func BenchmarkMonadMap_Right(b *testing.B) {
|
||||
rioe := Right[benchConfig](42)
|
||||
mapper := N.Mul(2)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = MonadMap(rioe, mapper)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMonadMap_Left(b *testing.B) {
|
||||
rioe := Left[benchConfig, int](benchErr)
|
||||
mapper := N.Mul(2)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = MonadMap(rioe, mapper)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMap_Right(b *testing.B) {
|
||||
rioe := Right[benchConfig](42)
|
||||
mapper := Map[benchConfig](N.Mul(2))
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = mapper(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMap_Left(b *testing.B) {
|
||||
rioe := Left[benchConfig, int](benchErr)
|
||||
mapper := Map[benchConfig](N.Mul(2))
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = mapper(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMapTo_Right(b *testing.B) {
|
||||
rioe := Right[benchConfig](42)
|
||||
mapper := MapTo[benchConfig, int](99)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = mapper(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark monad operations
|
||||
func BenchmarkMonadChain_Right(b *testing.B) {
|
||||
rioe := Right[benchConfig](42)
|
||||
chainer := func(a int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](a * 2) }
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = MonadChain(rioe, chainer)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMonadChain_Left(b *testing.B) {
|
||||
rioe := Left[benchConfig, int](benchErr)
|
||||
chainer := func(a int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](a * 2) }
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = MonadChain(rioe, chainer)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChain_Right(b *testing.B) {
|
||||
rioe := Right[benchConfig](42)
|
||||
chainer := Chain[benchConfig](func(a int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](a * 2) })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = chainer(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
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) })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = chainer(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChainFirst_Right(b *testing.B) {
|
||||
rioe := Right[benchConfig](42)
|
||||
chainer := ChainFirst[benchConfig](func(a int) ReaderIOResult[benchConfig, string] { return Right[benchConfig]("logged") })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = chainer(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChainFirst_Left(b *testing.B) {
|
||||
rioe := Left[benchConfig, int](benchErr)
|
||||
chainer := ChainFirst[benchConfig](func(a int) ReaderIOResult[benchConfig, string] { return Right[benchConfig]("logged") })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = chainer(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFlatten_Right(b *testing.B) {
|
||||
nested := Right[benchConfig](Right[benchConfig](42))
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = Flatten(nested)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFlatten_Left(b *testing.B) {
|
||||
nested := Left[benchConfig, ReaderIOResult[benchConfig, int]](benchErr)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = Flatten(nested)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark applicative operations
|
||||
func BenchmarkMonadApSeq_RightRight(b *testing.B) {
|
||||
fab := Right[benchConfig](N.Mul(2))
|
||||
fa := Right[benchConfig](42)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = MonadApSeq(fab, fa)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMonadApSeq_RightLeft(b *testing.B) {
|
||||
fab := Right[benchConfig](N.Mul(2))
|
||||
fa := Left[benchConfig, int](benchErr)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = MonadApSeq(fab, fa)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMonadApSeq_LeftRight(b *testing.B) {
|
||||
fab := Left[benchConfig, func(int) int](benchErr)
|
||||
fa := Right[benchConfig](42)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = MonadApSeq(fab, fa)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMonadApPar_RightRight(b *testing.B) {
|
||||
fab := Right[benchConfig](N.Mul(2))
|
||||
fa := Right[benchConfig](42)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = MonadApPar(fab, fa)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMonadApPar_RightLeft(b *testing.B) {
|
||||
fab := Right[benchConfig](N.Mul(2))
|
||||
fa := Left[benchConfig, int](benchErr)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = MonadApPar(fab, fa)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMonadApPar_LeftRight(b *testing.B) {
|
||||
fab := Left[benchConfig, func(int) int](benchErr)
|
||||
fa := Right[benchConfig](42)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = MonadApPar(fab, fa)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark execution of applicative operations
|
||||
func BenchmarkExecuteApSeq_RightRight(b *testing.B) {
|
||||
fab := Right[benchConfig](N.Mul(2))
|
||||
fa := Right[benchConfig](42)
|
||||
rioe := MonadApSeq(fab, fa)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
val, err := rioe(benchCfg)()
|
||||
if err != nil {
|
||||
benchResult = E.Left[int](err)
|
||||
} else {
|
||||
benchResult = E.Right[error](val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExecuteApPar_RightRight(b *testing.B) {
|
||||
fab := Right[benchConfig](N.Mul(2))
|
||||
fa := Right[benchConfig](42)
|
||||
rioe := MonadApPar(fab, fa)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
val, err := rioe(benchCfg)()
|
||||
if err != nil {
|
||||
benchResult = E.Left[int](err)
|
||||
} else {
|
||||
benchResult = E.Right[error](val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark chain operations with different types
|
||||
func BenchmarkChainEitherK_Right(b *testing.B) {
|
||||
rioe := Right[benchConfig](42)
|
||||
chainer := ChainEitherK[benchConfig](func(a int) E.Either[error, int] { return E.Right[error](a * 2) })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = chainer(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChainEitherK_Left(b *testing.B) {
|
||||
rioe := Left[benchConfig, int](benchErr)
|
||||
chainer := ChainEitherK[benchConfig](func(a int) E.Either[error, int] { return E.Right[error](a * 2) })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = chainer(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChainIOK_Right(b *testing.B) {
|
||||
rioe := Right[benchConfig](42)
|
||||
chainer := ChainIOK[benchConfig](func(a int) func() int { return func() int { return a * 2 } })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = chainer(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChainIOK_Left(b *testing.B) {
|
||||
rioe := Left[benchConfig, int](benchErr)
|
||||
chainer := ChainIOK[benchConfig](func(a int) func() int { return func() int { return a * 2 } })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = chainer(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark context operations
|
||||
func BenchmarkAsk(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_ = Ask[benchConfig]()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAsks(b *testing.B) {
|
||||
reader := func(cfg benchConfig) int { return cfg.value }
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_ = Asks(reader)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark pipeline operations
|
||||
func BenchmarkPipeline_Map_Right(b *testing.B) {
|
||||
rioe := Right[benchConfig](21)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = F.Pipe1(
|
||||
rioe,
|
||||
Map[benchConfig](N.Mul(2)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPipeline_Map_Left(b *testing.B) {
|
||||
rioe := Left[benchConfig, int](benchErr)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = F.Pipe1(
|
||||
rioe,
|
||||
Map[benchConfig](N.Mul(2)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPipeline_Chain_Right(b *testing.B) {
|
||||
rioe := Right[benchConfig](21)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = F.Pipe1(
|
||||
rioe,
|
||||
Chain[benchConfig](func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x * 2) }),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPipeline_Chain_Left(b *testing.B) {
|
||||
rioe := Left[benchConfig, int](benchErr)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = F.Pipe1(
|
||||
rioe,
|
||||
Chain[benchConfig](func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x * 2) }),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPipeline_Complex_Right(b *testing.B) {
|
||||
rioe := Right[benchConfig](10)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = F.Pipe3(
|
||||
rioe,
|
||||
Map[benchConfig](N.Mul(2)),
|
||||
Chain[benchConfig](func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x + 1) }),
|
||||
Map[benchConfig](N.Mul(2)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPipeline_Complex_Left(b *testing.B) {
|
||||
rioe := Left[benchConfig, int](benchErr)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = F.Pipe3(
|
||||
rioe,
|
||||
Map[benchConfig](N.Mul(2)),
|
||||
Chain[benchConfig](func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x + 1) }),
|
||||
Map[benchConfig](N.Mul(2)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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) }),
|
||||
Map[benchConfig](N.Mul(2)),
|
||||
)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
val, err := rioe(benchCfg)()
|
||||
if err != nil {
|
||||
benchResult = E.Left[int](err)
|
||||
} else {
|
||||
benchResult = E.Right[error](val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark Local operation
|
||||
func BenchmarkLocal(b *testing.B) {
|
||||
rioe := Asks(func(cfg benchConfig) int { return cfg.value })
|
||||
localOp := Local[int](func(cfg benchConfig) benchConfig { return benchConfig{value: cfg.value * 2} })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
benchRIOE = localOp(rioe)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExecuteLocal(b *testing.B) {
|
||||
rioe := Asks(func(cfg benchConfig) int { return cfg.value })
|
||||
localOp := Local[int](func(cfg benchConfig) benchConfig { return benchConfig{value: cfg.value * 2} })
|
||||
modified := localOp(rioe)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
val, err := modified(benchCfg)()
|
||||
if err != nil {
|
||||
benchResult = E.Left[int](err)
|
||||
} else {
|
||||
benchResult = E.Right[error](val)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -590,3 +590,283 @@ type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
func TestReadIO(t *testing.T) {
|
||||
t.Run("executes computation with IO environment", func(t *testing.T) {
|
||||
// IO that produces the config
|
||||
loadConfig := func() TestConfig {
|
||||
return TestConfig{Multiplier: 7, Prefix: "loaded"}
|
||||
}
|
||||
|
||||
// Computation that uses the config
|
||||
computation := func(cfg TestConfig) IOResult[string] {
|
||||
return func() (string, error) {
|
||||
return fmt.Sprintf("%s:%d", cfg.Prefix, cfg.Multiplier), nil
|
||||
}
|
||||
}
|
||||
|
||||
result := ReadIO[string](loadConfig)(computation)
|
||||
value, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "loaded:7", value)
|
||||
})
|
||||
|
||||
t.Run("executes IO before computation", func(t *testing.T) {
|
||||
executionOrder := []string{}
|
||||
|
||||
// IO that tracks execution
|
||||
loadConfig := func() TestConfig {
|
||||
executionOrder = append(executionOrder, "load-config")
|
||||
return TestConfig{Multiplier: 5}
|
||||
}
|
||||
|
||||
// Computation that tracks execution
|
||||
computation := func(cfg TestConfig) IOResult[int] {
|
||||
return func() (int, error) {
|
||||
executionOrder = append(executionOrder, "compute")
|
||||
return cfg.Multiplier * 10, nil
|
||||
}
|
||||
}
|
||||
|
||||
result := ReadIO[int](loadConfig)(computation)
|
||||
value, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 50, value)
|
||||
assert.Equal(t, []string{"load-config", "compute"}, executionOrder)
|
||||
})
|
||||
|
||||
t.Run("propagates computation error", func(t *testing.T) {
|
||||
expectedError := errors.New("computation failed")
|
||||
|
||||
loadConfig := func() TestConfig {
|
||||
return TestConfig{Multiplier: 5}
|
||||
}
|
||||
|
||||
computation := func(cfg TestConfig) IOResult[int] {
|
||||
return func() (int, error) {
|
||||
return 0, expectedError
|
||||
}
|
||||
}
|
||||
|
||||
result := ReadIO[int](loadConfig)(computation)
|
||||
_, err := result()
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedError, err)
|
||||
})
|
||||
|
||||
t.Run("works with different environment types", func(t *testing.T) {
|
||||
// Using a simple string as environment
|
||||
loadEnv := func() string {
|
||||
return "test-env"
|
||||
}
|
||||
|
||||
computation := func(env string) IOResult[string] {
|
||||
return func() (string, error) {
|
||||
return "env:" + env, nil
|
||||
}
|
||||
}
|
||||
|
||||
result := ReadIO[string](loadEnv)(computation)
|
||||
value, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "env:test-env", value)
|
||||
})
|
||||
|
||||
t.Run("IO is executed on each call", func(t *testing.T) {
|
||||
counter := 0
|
||||
loadConfig := func() TestConfig {
|
||||
counter++
|
||||
return TestConfig{Multiplier: counter}
|
||||
}
|
||||
|
||||
computation := func(cfg TestConfig) IOResult[int] {
|
||||
return func() (int, error) {
|
||||
return cfg.Multiplier, nil
|
||||
}
|
||||
}
|
||||
|
||||
result := ReadIO[int](loadConfig)(computation)
|
||||
|
||||
// First execution
|
||||
value1, _ := result()
|
||||
assert.Equal(t, 1, value1)
|
||||
|
||||
// Second execution - IO runs again
|
||||
value2, _ := result()
|
||||
assert.Equal(t, 2, value2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestReadIOResult(t *testing.T) {
|
||||
t.Run("executes computation with successful IOResult environment", func(t *testing.T) {
|
||||
// IOResult that successfully produces config
|
||||
loadConfig := func() (TestConfig, error) {
|
||||
return TestConfig{Multiplier: 8, Prefix: "success"}, nil
|
||||
}
|
||||
|
||||
// Computation that uses the config
|
||||
computation := func(cfg TestConfig) IOResult[string] {
|
||||
return func() (string, error) {
|
||||
return fmt.Sprintf("%s:%d", cfg.Prefix, cfg.Multiplier), nil
|
||||
}
|
||||
}
|
||||
|
||||
result := ReadIOResult[string](loadConfig)(computation)
|
||||
value, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "success:8", value)
|
||||
})
|
||||
|
||||
t.Run("propagates environment loading error", func(t *testing.T) {
|
||||
expectedError := errors.New("failed to load config")
|
||||
|
||||
// IOResult that fails to produce config
|
||||
loadConfig := func() (TestConfig, error) {
|
||||
return TestConfig{}, expectedError
|
||||
}
|
||||
|
||||
// Computation should not be executed
|
||||
computationCalled := false
|
||||
computation := func(cfg TestConfig) IOResult[string] {
|
||||
return func() (string, error) {
|
||||
computationCalled = true
|
||||
return "should not reach here", nil
|
||||
}
|
||||
}
|
||||
|
||||
result := ReadIOResult[string](loadConfig)(computation)
|
||||
_, err := result()
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedError, err)
|
||||
assert.False(t, computationCalled, "computation should not be called when environment loading fails")
|
||||
})
|
||||
|
||||
t.Run("propagates computation error after successful environment load", func(t *testing.T) {
|
||||
expectedError := errors.New("computation failed")
|
||||
|
||||
loadConfig := func() (TestConfig, error) {
|
||||
return TestConfig{Multiplier: 5}, nil
|
||||
}
|
||||
|
||||
computation := func(cfg TestConfig) IOResult[int] {
|
||||
return func() (int, error) {
|
||||
return 0, expectedError
|
||||
}
|
||||
}
|
||||
|
||||
result := ReadIOResult[int](loadConfig)(computation)
|
||||
_, err := result()
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedError, err)
|
||||
})
|
||||
|
||||
t.Run("chains environment loading and computation", func(t *testing.T) {
|
||||
executionOrder := []string{}
|
||||
|
||||
loadConfig := func() (TestConfig, error) {
|
||||
executionOrder = append(executionOrder, "load-config")
|
||||
return TestConfig{Multiplier: 3}, nil
|
||||
}
|
||||
|
||||
computation := func(cfg TestConfig) IOResult[int] {
|
||||
return func() (int, error) {
|
||||
executionOrder = append(executionOrder, "compute")
|
||||
return cfg.Multiplier * 10, nil
|
||||
}
|
||||
}
|
||||
|
||||
result := ReadIOResult[int](loadConfig)(computation)
|
||||
value, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 30, value)
|
||||
assert.Equal(t, []string{"load-config", "compute"}, executionOrder)
|
||||
})
|
||||
|
||||
t.Run("works with validation in environment loading", func(t *testing.T) {
|
||||
// IOResult that validates config
|
||||
loadConfig := func() (TestConfig, error) {
|
||||
cfg := TestConfig{Multiplier: -1}
|
||||
if cfg.Multiplier < 0 {
|
||||
return TestConfig{}, errors.New("invalid multiplier: must be positive")
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
computation := func(cfg TestConfig) IOResult[int] {
|
||||
return func() (int, error) {
|
||||
return cfg.Multiplier * 10, nil
|
||||
}
|
||||
}
|
||||
|
||||
result := ReadIOResult[int](loadConfig)(computation)
|
||||
_, err := result()
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid multiplier")
|
||||
})
|
||||
|
||||
t.Run("IOResult is executed on each call", func(t *testing.T) {
|
||||
counter := 0
|
||||
loadConfig := func() (TestConfig, error) {
|
||||
counter++
|
||||
if counter == 1 {
|
||||
return TestConfig{}, errors.New("first attempt fails")
|
||||
}
|
||||
return TestConfig{Multiplier: counter}, nil
|
||||
}
|
||||
|
||||
computation := func(cfg TestConfig) IOResult[int] {
|
||||
return func() (int, error) {
|
||||
return cfg.Multiplier, nil
|
||||
}
|
||||
}
|
||||
|
||||
result := ReadIOResult[int](loadConfig)(computation)
|
||||
|
||||
// First execution - fails
|
||||
_, err1 := result()
|
||||
assert.Error(t, err1)
|
||||
|
||||
// Second execution - succeeds
|
||||
value2, err2 := result()
|
||||
assert.NoError(t, err2)
|
||||
assert.Equal(t, 2, value2)
|
||||
})
|
||||
|
||||
t.Run("works with complex environment types", func(t *testing.T) {
|
||||
type DatabaseConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
Username string
|
||||
}
|
||||
|
||||
loadDBConfig := func() (DatabaseConfig, error) {
|
||||
// Simulate loading from environment variables
|
||||
return DatabaseConfig{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
Username: "admin",
|
||||
}, nil
|
||||
}
|
||||
|
||||
computation := func(cfg DatabaseConfig) IOResult[string] {
|
||||
return func() (string, error) {
|
||||
return fmt.Sprintf("%s@%s:%d", cfg.Username, cfg.Host, cfg.Port), nil
|
||||
}
|
||||
}
|
||||
|
||||
result := ReadIOResult[string](loadDBConfig)(computation)
|
||||
value, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "admin@localhost:5432", value)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -60,3 +60,5 @@ type Apply[A, B, HKTA, HKTB, HKTFAB any] interface {
|
||||
func ToFunctor[A, B, HKTA, HKTB, HKTFAB any](ap Apply[A, B, HKTA, HKTB, HKTFAB]) functor.Functor[A, B, HKTA, HKTB] {
|
||||
return ap
|
||||
}
|
||||
|
||||
type ApType[HKTA, HKTB, HKTFAB any] = func(HKTA) func(HKTFAB) HKTB
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -47,3 +47,5 @@ type Functor[A, B, HKTA, HKTB any] interface {
|
||||
// Returns a function that takes a functor containing A and returns a functor containing B.
|
||||
Map(func(A) B) func(HKTA) HKTB
|
||||
}
|
||||
|
||||
type MapType[A, B, HKTA, HKTB any] = func(func(A) B) func(HKTA) HKTB
|
||||
|
||||
@@ -37,3 +37,5 @@ type Pointed[A, HKTA any] interface {
|
||||
// creating a valid instance of the higher-kinded type.
|
||||
Of(A) HKTA
|
||||
}
|
||||
|
||||
type OfType[A, HKTA any] = func(A) HKTA
|
||||
|
||||
@@ -17,6 +17,7 @@ package readert
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/chain"
|
||||
R "github.com/IBM/fp-go/v2/reader/generic"
|
||||
)
|
||||
|
||||
@@ -51,7 +52,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 {
|
||||
|
||||
119
v2/internal/traversable/types.go
Normal file
119
v2/internal/traversable/types.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package traversable
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/applicative"
|
||||
"github.com/IBM/fp-go/v2/internal/apply"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/pointed"
|
||||
)
|
||||
|
||||
type (
|
||||
Pointed[A, HKT_A any] = pointed.Pointed[A, HKT_A]
|
||||
Functor[A, B, HKT_A, HKT_B any] = functor.Functor[A, B, HKT_A, HKT_B]
|
||||
Applicative[A, B, HKT_A, HKT_B, HKT_AB any] = applicative.Applicative[A, B, HKT_A, HKT_B, HKT_AB]
|
||||
|
||||
TraverseType[A, B, HKT_T_A, HKT_T_B, HKT_F_B, HKT_F_T_B, HKT_F_T_A_B any] = func(
|
||||
// applicative F
|
||||
f_of pointed.OfType[HKT_T_B, HKT_F_T_B],
|
||||
f_map functor.MapType[HKT_T_B, func(B) HKT_T_B, HKT_F_T_B, HKT_F_T_A_B],
|
||||
f_ap apply.ApType[HKT_F_B, HKT_F_T_B, HKT_F_T_A_B],
|
||||
|
||||
) func(func(A) HKT_F_B) func(HKT_T_A) HKT_F_T_B
|
||||
|
||||
SequenceType[
|
||||
HKT_T_F_A,
|
||||
HKT_F_T_A any] = func(
|
||||
// applicative F
|
||||
f_of pointed.OfType[HKT_T_F_A, HKT_F_T_A],
|
||||
f_map functor.MapType[HKT_T_F_A, func(HKT_T_F_A) HKT_T_F_A, HKT_F_T_A, HKT_T_F_A],
|
||||
f_ap apply.ApType[HKT_T_F_A, HKT_F_T_A, HKT_T_F_A],
|
||||
) func(HKT_T_F_A) HKT_F_T_A
|
||||
)
|
||||
|
||||
func ComposeTraverse[
|
||||
A,
|
||||
B,
|
||||
HKT_F_B,
|
||||
HKT_G_A,
|
||||
HKT_G_B,
|
||||
HKT_T_G_A,
|
||||
HKT_T_G_B,
|
||||
HKT_F_G_B,
|
||||
HKT_F_T_G_B,
|
||||
HKT_F_T_A_B any](
|
||||
t TraverseType[HKT_G_A, HKT_G_B, HKT_T_G_A, HKT_T_G_B, HKT_F_G_B, HKT_F_T_G_B, HKT_F_T_A_B],
|
||||
g TraverseType[A, B, HKT_G_A, HKT_G_B, HKT_F_B, HKT_F_G_B, HKT_F_T_A_B],
|
||||
) func(
|
||||
// applicative F
|
||||
f_of pointed.OfType[HKT_T_G_B, HKT_F_T_G_B],
|
||||
f_map functor.MapType[HKT_T_G_B, func(HKT_G_B) HKT_T_G_B, HKT_F_T_G_B, HKT_F_T_A_B],
|
||||
f_ap apply.ApType[HKT_F_G_B, HKT_F_T_G_B, HKT_F_T_A_B],
|
||||
|
||||
// applicative G
|
||||
g_of pointed.OfType[HKT_G_B, HKT_F_G_B],
|
||||
g_map functor.MapType[HKT_G_B, func(B) HKT_G_B, HKT_F_G_B, HKT_F_T_A_B],
|
||||
g_ap apply.ApType[HKT_F_B, HKT_F_G_B, HKT_F_T_A_B],
|
||||
) func(func(A) HKT_F_B) func(HKT_T_G_A) HKT_F_T_G_B {
|
||||
|
||||
return func(
|
||||
// applicative F
|
||||
f_of pointed.OfType[HKT_T_G_B, HKT_F_T_G_B],
|
||||
f_map functor.MapType[HKT_T_G_B, func(HKT_G_B) HKT_T_G_B, HKT_F_T_G_B, HKT_F_T_A_B],
|
||||
f_ap apply.ApType[HKT_F_G_B, HKT_F_T_G_B, HKT_F_T_A_B],
|
||||
|
||||
// applicative G
|
||||
g_of pointed.OfType[HKT_G_B, HKT_F_G_B],
|
||||
g_map functor.MapType[HKT_G_B, func(B) HKT_G_B, HKT_F_G_B, HKT_F_T_A_B],
|
||||
g_ap apply.ApType[HKT_F_B, HKT_F_G_B, HKT_F_T_A_B],
|
||||
|
||||
) func(func(A) HKT_F_B) func(HKT_T_G_A) HKT_F_T_G_B {
|
||||
|
||||
return F.Flow2(
|
||||
g(g_of, g_map, g_ap),
|
||||
t(f_of, f_map, f_ap),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// func ComposeSequence[
|
||||
// HKT_F_G_A,
|
||||
// HKT_G_F_A,
|
||||
// HKT_T_G_F_A,
|
||||
// HKT_F_T_G_A,
|
||||
// HKT_T_F_G_A any](
|
||||
|
||||
// t SequenceType[HKT_T_F_G_A, HKT_F_T_G_A],
|
||||
// t_map functor.MapType[HKT_G_F_A, HKT_F_G_A, HKT_T_G_F_A, HKT_T_F_G_A],
|
||||
|
||||
// g SequenceType[HKT_G_F_A, HKT_F_G_A],
|
||||
// ) func(
|
||||
// // applicative F
|
||||
// ) func(HKT_T_G_F_A) HKT_F_T_G_A {
|
||||
|
||||
// return func() func(HKT_T_G_F_A) HKT_F_T_G_A {
|
||||
// return F.Flow2(
|
||||
// t_map(g()),
|
||||
// t(),
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
func SequenceFromTraverse[
|
||||
A, HKT_T_A, HKT_F_B, HKT_F_T_B any](
|
||||
t TraverseType[HKT_T_A, HKT_T_A, HKT_T_A, HKT_T_A, HKT_T_A, HKT_F_T_B, HKT_T_A],
|
||||
) SequenceType[HKT_T_A, HKT_F_T_B] {
|
||||
|
||||
return func(
|
||||
// applicative F
|
||||
f_of pointed.OfType[HKT_T_A, HKT_F_T_B],
|
||||
f_map functor.MapType[HKT_T_A, func(HKT_T_A) HKT_T_A, HKT_F_T_B, HKT_T_A],
|
||||
f_ap apply.ApType[HKT_T_A, HKT_F_T_B, HKT_T_A],
|
||||
) func(HKT_T_A) HKT_F_T_B {
|
||||
return F.Pipe1(
|
||||
F.Identity[HKT_T_A],
|
||||
t(f_of, f_map, f_ap),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -21,20 +21,68 @@ import (
|
||||
|
||||
type (
|
||||
ioApplicative[A, B any] struct{}
|
||||
|
||||
// IOApplicative represents the applicative functor type class for IO.
|
||||
// It combines the capabilities of Functor (Map) and Pointed (Of) with
|
||||
// the ability to apply wrapped functions to wrapped values (Ap).
|
||||
//
|
||||
// An applicative functor is a functor with two additional operations:
|
||||
// - Of: lifts a pure value into the IO context
|
||||
// - Ap: applies a wrapped function to a wrapped value
|
||||
//
|
||||
// This allows for function application within the IO context while maintaining
|
||||
// the computational structure. The Ap operation uses parallel execution by default
|
||||
// for better performance.
|
||||
//
|
||||
// Type parameters:
|
||||
// - A: the input type
|
||||
// - B: the output type
|
||||
IOApplicative[A, B any] = applicative.Applicative[A, B, IO[A], IO[B], IO[func(A) B]]
|
||||
)
|
||||
|
||||
// Of lifts a pure value into the IO context.
|
||||
// This is the pointed functor operation that wraps a value in an IO computation.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// app := io.Applicative[int, string]()
|
||||
// ioValue := app.Of(42) // IO[int] that returns 42
|
||||
// result := ioValue() // 42
|
||||
func (o *ioApplicative[A, B]) Of(a A) IO[A] {
|
||||
return Of(a)
|
||||
}
|
||||
|
||||
// Map transforms the result of an IO computation by applying a function to it.
|
||||
// This is the functor operation that allows mapping over wrapped values.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// app := io.Applicative[int, string]()
|
||||
// double := func(x int) int { return x * 2 }
|
||||
// ioValue := app.Of(21)
|
||||
// doubled := app.Map(double)(ioValue)
|
||||
// result := doubled() // 42
|
||||
func (o *ioApplicative[A, B]) Map(f func(A) B) Operator[A, B] {
|
||||
return Map(f)
|
||||
}
|
||||
|
||||
// Ap applies a wrapped function to a wrapped value, both in the IO context.
|
||||
// This operation uses parallel execution by default, running the function and
|
||||
// value computations concurrently for better performance.
|
||||
//
|
||||
// The Ap operation is useful for applying multi-argument functions in a curried
|
||||
// fashion within the IO context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// app := io.Applicative[int, int]()
|
||||
// add := func(a int) func(int) int {
|
||||
// return func(b int) int { return a + b }
|
||||
// }
|
||||
// ioFunc := app.Of(add(10)) // IO[func(int) int]
|
||||
// ioValue := app.Of(32) // IO[int]
|
||||
// result := app.Ap(ioValue)(ioFunc)
|
||||
// value := result() // 42
|
||||
func (o *ioApplicative[A, B]) Ap(fa IO[A]) Operator[func(A) B, B] {
|
||||
return Ap[B](fa)
|
||||
}
|
||||
@@ -43,10 +91,45 @@ func (o *ioApplicative[A, B]) Ap(fa IO[A]) Operator[func(A) B, B] {
|
||||
// This provides a structured way to access applicative operations (Of, Map, Ap)
|
||||
// for IO computations.
|
||||
//
|
||||
// Example:
|
||||
// The applicative pattern is useful when you need to:
|
||||
// - Apply functions with multiple arguments to wrapped values
|
||||
// - Combine multiple independent IO computations
|
||||
// - Maintain the computational structure while transforming values
|
||||
//
|
||||
// Type parameters:
|
||||
// - A: the input type for the applicative operations
|
||||
// - B: the output type for the applicative operations
|
||||
//
|
||||
// Example - Basic usage:
|
||||
//
|
||||
// app := io.Applicative[int, string]()
|
||||
// result := app.Map(strconv.Itoa)(app.Of(42))
|
||||
// value := result() // "42"
|
||||
//
|
||||
// Example - Applying curried functions:
|
||||
//
|
||||
// app := io.Applicative[int, int]()
|
||||
// add := func(a int) func(int) int {
|
||||
// return func(b int) int { return a + b }
|
||||
// }
|
||||
// // Create IO computations
|
||||
// ioFunc := io.Map(add)(app.Of(10)) // IO[func(int) int]
|
||||
// ioValue := app.Of(32) // IO[int]
|
||||
// // Apply the function to the value
|
||||
// result := app.Ap(ioValue)(ioFunc)
|
||||
// value := result() // 42
|
||||
//
|
||||
// Example - Combining multiple IO computations:
|
||||
//
|
||||
// app := io.Applicative[int, int]()
|
||||
// multiply := func(a int) func(int) int {
|
||||
// return func(b int) int { return a * b }
|
||||
// }
|
||||
// io1 := app.Of(6)
|
||||
// io2 := app.Of(7)
|
||||
// ioFunc := io.Map(multiply)(io1)
|
||||
// result := app.Ap(io2)(ioFunc)
|
||||
// value := result() // 42
|
||||
func Applicative[A, B any]() IOApplicative[A, B] {
|
||||
return &ioApplicative[A, B]{}
|
||||
}
|
||||
|
||||
360
v2/io/applicative_test.go
Normal file
360
v2/io/applicative_test.go
Normal file
@@ -0,0 +1,360 @@
|
||||
// Copyright (c) 2024 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package io
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestApplicativeOf tests the Of operation of the Applicative type class
|
||||
func TestApplicativeOf(t *testing.T) {
|
||||
app := Applicative[int, string]()
|
||||
|
||||
t.Run("wraps a value in IO context", func(t *testing.T) {
|
||||
ioValue := app.Of(42)
|
||||
result := ioValue()
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
|
||||
t.Run("wraps string value", func(t *testing.T) {
|
||||
app := Applicative[string, int]()
|
||||
ioValue := app.Of("hello")
|
||||
result := ioValue()
|
||||
assert.Equal(t, "hello", result)
|
||||
})
|
||||
|
||||
t.Run("wraps zero value", func(t *testing.T) {
|
||||
ioValue := app.Of(0)
|
||||
result := ioValue()
|
||||
assert.Equal(t, 0, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMap tests the Map operation of the Applicative type class
|
||||
func TestApplicativeMap(t *testing.T) {
|
||||
app := Applicative[int, int]()
|
||||
|
||||
t.Run("maps a function over IO value", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
ioValue := app.Of(21)
|
||||
result := app.Map(double)(ioValue)
|
||||
assert.Equal(t, 42, result())
|
||||
})
|
||||
|
||||
t.Run("maps type conversion", func(t *testing.T) {
|
||||
app := Applicative[int, string]()
|
||||
ioValue := app.Of(42)
|
||||
result := app.Map(strconv.Itoa)(ioValue)
|
||||
assert.Equal(t, "42", result())
|
||||
})
|
||||
|
||||
t.Run("maps identity function", func(t *testing.T) {
|
||||
identity := func(x int) int { return x }
|
||||
ioValue := app.Of(42)
|
||||
result := app.Map(identity)(ioValue)
|
||||
assert.Equal(t, 42, result())
|
||||
})
|
||||
|
||||
t.Run("maps constant function", func(t *testing.T) {
|
||||
constant := func(x int) int { return 100 }
|
||||
ioValue := app.Of(42)
|
||||
result := app.Map(constant)(ioValue)
|
||||
assert.Equal(t, 100, result())
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeAp tests the Ap operation of the Applicative type class
|
||||
func TestApplicativeAp(t *testing.T) {
|
||||
t.Run("applies wrapped function to wrapped value", func(t *testing.T) {
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
ioFunc := Of(add(10))
|
||||
ioValue := Of(32)
|
||||
result := Ap[int](ioValue)(ioFunc)
|
||||
assert.Equal(t, 42, result())
|
||||
})
|
||||
|
||||
t.Run("applies multiplication function", func(t *testing.T) {
|
||||
multiply := func(a int) func(int) int {
|
||||
return func(b int) int { return a * b }
|
||||
}
|
||||
ioFunc := Of(multiply(6))
|
||||
ioValue := Of(7)
|
||||
result := Ap[int](ioValue)(ioFunc)
|
||||
assert.Equal(t, 42, result())
|
||||
})
|
||||
|
||||
t.Run("applies function with zero", func(t *testing.T) {
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
ioFunc := Of(add(0))
|
||||
ioValue := Of(42)
|
||||
result := Ap[int](ioValue)(ioFunc)
|
||||
assert.Equal(t, 42, result())
|
||||
})
|
||||
|
||||
t.Run("applies with type conversion", func(t *testing.T) {
|
||||
toStringAndAppend := func(suffix string) func(int) string {
|
||||
return func(n int) string {
|
||||
return strconv.Itoa(n) + suffix
|
||||
}
|
||||
}
|
||||
ioFunc := Of(toStringAndAppend("!"))
|
||||
ioValue := Of(42)
|
||||
result := Ap[string](ioValue)(ioFunc)
|
||||
assert.Equal(t, "42!", result())
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeComposition tests composition of applicative operations
|
||||
func TestApplicativeComposition(t *testing.T) {
|
||||
app := Applicative[int, int]()
|
||||
|
||||
t.Run("composes Map and Of", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
result := F.Pipe1(
|
||||
app.Of(21),
|
||||
app.Map(double),
|
||||
)
|
||||
assert.Equal(t, 42, result())
|
||||
})
|
||||
|
||||
t.Run("composes multiple Map operations", func(t *testing.T) {
|
||||
app := Applicative[int, string]()
|
||||
double := func(x int) int { return x * 2 }
|
||||
toString := func(x int) string { return strconv.Itoa(x) }
|
||||
|
||||
result := F.Pipe2(
|
||||
app.Of(21),
|
||||
Map(double),
|
||||
app.Map(toString),
|
||||
)
|
||||
assert.Equal(t, "42", result())
|
||||
})
|
||||
|
||||
t.Run("composes Map and Ap", func(t *testing.T) {
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
|
||||
ioFunc := F.Pipe1(
|
||||
app.Of(5),
|
||||
Map(add),
|
||||
)
|
||||
ioValue := app.Of(16)
|
||||
|
||||
result := Ap[int](ioValue)(ioFunc)
|
||||
assert.Equal(t, 21, result())
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeLaws tests the applicative functor laws
|
||||
func TestApplicativeLaws(t *testing.T) {
|
||||
app := Applicative[int, int]()
|
||||
|
||||
t.Run("identity law: ap(Of(id), v) = v", func(t *testing.T) {
|
||||
identity := func(x int) int { return x }
|
||||
v := app.Of(42)
|
||||
|
||||
left := Ap[int](v)(Of(identity))
|
||||
right := v
|
||||
|
||||
assert.Equal(t, right(), left())
|
||||
})
|
||||
|
||||
t.Run("homomorphism law: ap(Of(f), Of(x)) = Of(f(x))", func(t *testing.T) {
|
||||
f := func(x int) int { return x * 2 }
|
||||
x := 21
|
||||
|
||||
left := Ap[int](app.Of(x))(Of(f))
|
||||
right := app.Of(f(x))
|
||||
|
||||
assert.Equal(t, right(), left())
|
||||
})
|
||||
|
||||
t.Run("interchange law: ap(u, Of(y)) = ap(Of(f => f(y)), u)", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
u := Of(double)
|
||||
y := 21
|
||||
|
||||
left := Ap[int](app.Of(y))(u)
|
||||
|
||||
applyY := func(f func(int) int) int { return f(y) }
|
||||
right := Ap[int](u)(Of(applyY))
|
||||
|
||||
assert.Equal(t, right(), left())
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeWithPipe tests applicative operations with pipe
|
||||
func TestApplicativeWithPipe(t *testing.T) {
|
||||
t.Run("pipes Of and Map", func(t *testing.T) {
|
||||
app := Applicative[int, string]()
|
||||
result := F.Pipe1(
|
||||
app.Of(42),
|
||||
app.Map(strconv.Itoa),
|
||||
)
|
||||
assert.Equal(t, "42", result())
|
||||
})
|
||||
|
||||
t.Run("pipes complex transformation", func(t *testing.T) {
|
||||
app := Applicative[int, int]()
|
||||
add10 := func(x int) int { return x + 10 }
|
||||
double := func(x int) int { return x * 2 }
|
||||
|
||||
result := F.Pipe2(
|
||||
app.Of(16),
|
||||
app.Map(add10),
|
||||
app.Map(double),
|
||||
)
|
||||
assert.Equal(t, 52, result())
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeWithUtils tests applicative with utility functions
|
||||
func TestApplicativeWithUtils(t *testing.T) {
|
||||
app := Applicative[int, int]()
|
||||
|
||||
t.Run("uses utils.Double", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
app.Of(21),
|
||||
app.Map(utils.Double),
|
||||
)
|
||||
assert.Equal(t, 42, result())
|
||||
})
|
||||
|
||||
t.Run("uses utils.Inc", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
app.Of(41),
|
||||
app.Map(utils.Inc),
|
||||
)
|
||||
assert.Equal(t, 42, result())
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMultipleArguments tests applying functions with multiple arguments
|
||||
func TestApplicativeMultipleArguments(t *testing.T) {
|
||||
app := Applicative[int, int]()
|
||||
|
||||
t.Run("applies curried two-argument function", func(t *testing.T) {
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
|
||||
// Create IO with curried function
|
||||
ioFunc := F.Pipe1(
|
||||
app.Of(10),
|
||||
Map(add),
|
||||
)
|
||||
|
||||
// Apply to second argument
|
||||
result := Ap[int](app.Of(32))(ioFunc)
|
||||
assert.Equal(t, 42, result())
|
||||
})
|
||||
|
||||
t.Run("applies curried three-argument function", func(t *testing.T) {
|
||||
add3 := func(a int) func(int) func(int) int {
|
||||
return func(b int) func(int) int {
|
||||
return func(c int) int {
|
||||
return a + b + c
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build up the computation step by step
|
||||
ioFunc1 := F.Pipe1(
|
||||
app.Of(10),
|
||||
Map(add3),
|
||||
)
|
||||
|
||||
ioFunc2 := Ap[func(int) int](app.Of(20))(ioFunc1)
|
||||
result := Ap[int](app.Of(12))(ioFunc2)
|
||||
|
||||
assert.Equal(t, 42, result())
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeParallelExecution tests that Ap uses parallel execution
|
||||
func TestApplicativeParallelExecution(t *testing.T) {
|
||||
t.Run("executes function and value in parallel", func(t *testing.T) {
|
||||
// This test verifies that both computations happen
|
||||
// The actual parallelism is tested by the implementation
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
|
||||
ioFunc := Of(add(20))
|
||||
ioValue := Of(22)
|
||||
|
||||
result := Ap[int](ioValue)(ioFunc)
|
||||
assert.Equal(t, 42, result())
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeInstance tests that Applicative returns a valid instance
|
||||
func TestApplicativeInstance(t *testing.T) {
|
||||
t.Run("returns non-nil instance", func(t *testing.T) {
|
||||
app := Applicative[int, string]()
|
||||
assert.NotNil(t, app)
|
||||
})
|
||||
|
||||
t.Run("multiple calls return independent instances", func(t *testing.T) {
|
||||
app1 := Applicative[int, string]()
|
||||
app2 := Applicative[int, string]()
|
||||
|
||||
// Both should work independently
|
||||
result1 := app1.Of(42)
|
||||
result2 := app2.Of(43)
|
||||
|
||||
assert.Equal(t, 42, result1())
|
||||
assert.Equal(t, 43, result2())
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeWithDifferentTypes tests applicative with various type combinations
|
||||
func TestApplicativeWithDifferentTypes(t *testing.T) {
|
||||
t.Run("int to string", func(t *testing.T) {
|
||||
app := Applicative[int, string]()
|
||||
result := app.Map(strconv.Itoa)(app.Of(42))
|
||||
assert.Equal(t, "42", result())
|
||||
})
|
||||
|
||||
t.Run("string to int", func(t *testing.T) {
|
||||
app := Applicative[string, int]()
|
||||
toLength := func(s string) int { return len(s) }
|
||||
result := app.Map(toLength)(app.Of("hello"))
|
||||
assert.Equal(t, 5, result())
|
||||
})
|
||||
|
||||
t.Run("bool to string", func(t *testing.T) {
|
||||
app := Applicative[bool, string]()
|
||||
toString := func(b bool) string {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
result := app.Map(toString)(app.Of(true))
|
||||
assert.Equal(t, "true", result())
|
||||
})
|
||||
}
|
||||
@@ -13,6 +13,51 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package file provides IO operations for file system interactions.
|
||||
//
|
||||
// This package offers functional wrappers around common file operations,
|
||||
// returning IO monads that encapsulate side effects. All operations are
|
||||
// lazy and only execute when the returned IO is invoked.
|
||||
//
|
||||
// # Core Operations
|
||||
//
|
||||
// The package provides two main operations:
|
||||
// - Close: Safely close io.Closer resources
|
||||
// - Remove: Remove files from the file system
|
||||
//
|
||||
// Both operations ignore errors and return the original input, making them
|
||||
// suitable for cleanup operations where errors should not interrupt the flow.
|
||||
//
|
||||
// # Basic Usage
|
||||
//
|
||||
// // Close a file
|
||||
// file, _ := os.Open("data.txt")
|
||||
// closeIO := file.Close(file)
|
||||
// closeIO() // Closes the file, ignoring any error
|
||||
//
|
||||
// // Remove a file
|
||||
// removeIO := file.Remove("temp.txt")
|
||||
// removeIO() // Removes the file, ignoring any error
|
||||
//
|
||||
// # Composition with IO
|
||||
//
|
||||
// These operations can be composed with other IO operations:
|
||||
//
|
||||
// result := pipe.Pipe2(
|
||||
// openFile("data.txt"),
|
||||
// io.ChainFirst(processFile),
|
||||
// io.Chain(file.Close),
|
||||
// )
|
||||
//
|
||||
// # Error Handling
|
||||
//
|
||||
// Both Close and Remove intentionally ignore errors. This design is suitable
|
||||
// for cleanup operations where:
|
||||
// - The operation is best-effort
|
||||
// - Errors should not interrupt the program flow
|
||||
// - The resource state is not critical
|
||||
//
|
||||
// For operations requiring error handling, use ioeither or ioresult instead.
|
||||
package file
|
||||
|
||||
import (
|
||||
@@ -22,7 +67,36 @@ import (
|
||||
IO "github.com/IBM/fp-go/v2/io"
|
||||
)
|
||||
|
||||
// Close closes a closeable resource and ignores a potential error
|
||||
// Close closes a closeable resource and ignores any potential error.
|
||||
// Returns an IO that, when executed, closes the resource and returns it.
|
||||
//
|
||||
// This function is useful for cleanup operations where errors can be safely
|
||||
// ignored, such as in defer statements or resource cleanup chains.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: Any type that implements io.Closer
|
||||
//
|
||||
// Parameters:
|
||||
// - r: The resource to close
|
||||
//
|
||||
// Returns:
|
||||
// - IO[R]: An IO computation that closes the resource and returns it
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// file, _ := os.Open("data.txt")
|
||||
// defer file.Close(file)() // Close when function returns
|
||||
//
|
||||
// Example with IO composition:
|
||||
//
|
||||
// result := pipe.Pipe3(
|
||||
// openFile("data.txt"),
|
||||
// io.Chain(readContent),
|
||||
// io.ChainFirst(file.Close),
|
||||
// )
|
||||
//
|
||||
// Note: The #nosec comment is intentional - errors are deliberately ignored
|
||||
// for cleanup operations where failure should not interrupt the flow.
|
||||
func Close[R io.Closer](r R) IO.IO[R] {
|
||||
return func() R {
|
||||
r.Close() // #nosec: G104
|
||||
@@ -30,7 +104,42 @@ func Close[R io.Closer](r R) IO.IO[R] {
|
||||
}
|
||||
}
|
||||
|
||||
// Remove removes a resource and ignores a potential error
|
||||
// Remove removes a file or directory and ignores any potential error.
|
||||
// Returns an IO that, when executed, removes the named file or directory
|
||||
// and returns the name.
|
||||
//
|
||||
// This function is useful for cleanup operations where errors can be safely
|
||||
// ignored, such as removing temporary files or cache directories.
|
||||
//
|
||||
// Parameters:
|
||||
// - name: The path to the file or directory to remove
|
||||
//
|
||||
// Returns:
|
||||
// - IO[string]: An IO computation that removes the file and returns the name
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// cleanup := file.Remove("temp.txt")
|
||||
// cleanup() // Removes temp.txt, ignoring any error
|
||||
//
|
||||
// Example with multiple files:
|
||||
//
|
||||
// cleanup := pipe.Pipe2(
|
||||
// file.Remove("temp1.txt"),
|
||||
// io.ChainTo(file.Remove("temp2.txt")),
|
||||
// )
|
||||
// cleanup() // Removes both files
|
||||
//
|
||||
// Example in defer:
|
||||
//
|
||||
// tempFile := "temp.txt"
|
||||
// defer file.Remove(tempFile)()
|
||||
// // ... use tempFile ...
|
||||
//
|
||||
// Note: The #nosec comment is intentional - errors are deliberately ignored
|
||||
// for cleanup operations where failure should not interrupt the flow.
|
||||
// This function only removes the named file or empty directory. To remove
|
||||
// a directory and its contents, use os.RemoveAll wrapped in an IO.
|
||||
func Remove(name string) IO.IO[string] {
|
||||
return func() string {
|
||||
os.Remove(name) // #nosec: G104
|
||||
|
||||
405
v2/io/file/file_test.go
Normal file
405
v2/io/file/file_test.go
Normal file
@@ -0,0 +1,405 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
IO "github.com/IBM/fp-go/v2/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// mockCloser is a mock implementation of io.Closer for testing
|
||||
type mockCloser struct {
|
||||
closed bool
|
||||
closeErr error
|
||||
closeFunc func() error
|
||||
}
|
||||
|
||||
func (m *mockCloser) Close() error {
|
||||
m.closed = true
|
||||
if m.closeFunc != nil {
|
||||
return m.closeFunc()
|
||||
}
|
||||
return m.closeErr
|
||||
}
|
||||
|
||||
// TestClose_WithMockCloser tests the Close function with a mock closer
|
||||
func TestClose_WithMockCloser(t *testing.T) {
|
||||
t.Run("closes resource successfully", func(t *testing.T) {
|
||||
mock := &mockCloser{}
|
||||
closeIO := Close(mock)
|
||||
|
||||
result := closeIO()
|
||||
|
||||
assert.True(t, mock.closed, "resource should be closed")
|
||||
assert.Equal(t, mock, result, "should return the same resource")
|
||||
})
|
||||
|
||||
t.Run("ignores close error", func(t *testing.T) {
|
||||
mock := &mockCloser{
|
||||
closeErr: fmt.Errorf("close error"),
|
||||
}
|
||||
closeIO := Close(mock)
|
||||
|
||||
// Should not panic even with error
|
||||
result := closeIO()
|
||||
|
||||
assert.True(t, mock.closed, "resource should be closed despite error")
|
||||
assert.Equal(t, mock, result, "should return the same resource")
|
||||
})
|
||||
|
||||
t.Run("can be called multiple times", func(t *testing.T) {
|
||||
mock := &mockCloser{}
|
||||
closeIO := Close(mock)
|
||||
|
||||
result1 := closeIO()
|
||||
result2 := closeIO()
|
||||
|
||||
assert.True(t, mock.closed, "resource should be closed")
|
||||
assert.Equal(t, result1, result2, "should return same resource each time")
|
||||
})
|
||||
}
|
||||
|
||||
// TestClose_WithBytesBuffer tests Close with bytes.Buffer (implements io.Closer)
|
||||
func TestClose_WithBytesBuffer(t *testing.T) {
|
||||
t.Run("closes bytes.Buffer", func(t *testing.T) {
|
||||
buf := bytes.NewBuffer([]byte("test data"))
|
||||
closeIO := Close(io.NopCloser(buf))
|
||||
|
||||
result := closeIO()
|
||||
|
||||
assert.NotNil(t, result, "should return the closer")
|
||||
})
|
||||
}
|
||||
|
||||
// TestClose_WithFile tests Close with actual file
|
||||
func TestClose_WithFile(t *testing.T) {
|
||||
t.Run("closes real file", func(t *testing.T) {
|
||||
// Create a temporary file
|
||||
tmpFile, err := os.CreateTemp("", "test-close-*.txt")
|
||||
require.NoError(t, err)
|
||||
tmpPath := tmpFile.Name()
|
||||
defer os.Remove(tmpPath)
|
||||
|
||||
// Write some data
|
||||
_, err = tmpFile.WriteString("test data")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Close using our function
|
||||
closeIO := Close(tmpFile)
|
||||
result := closeIO()
|
||||
|
||||
assert.Equal(t, tmpFile, result, "should return the same file")
|
||||
|
||||
// Verify file is closed by trying to write (should fail)
|
||||
_, err = tmpFile.WriteString("more data")
|
||||
assert.Error(t, err, "writing to closed file should fail")
|
||||
})
|
||||
}
|
||||
|
||||
// TestClose_Composition tests Close in IO composition
|
||||
func TestClose_Composition(t *testing.T) {
|
||||
t.Run("composes with other IO operations", func(t *testing.T) {
|
||||
mock := &mockCloser{}
|
||||
|
||||
// Create a pipeline that uses the resource and then closes it
|
||||
step1 := IO.Of(mock)
|
||||
step2 := IO.Map(func(m *mockCloser) *mockCloser {
|
||||
// Simulate using the resource
|
||||
return m
|
||||
})(step1)
|
||||
pipeline := IO.Chain(Close[*mockCloser])(step2)
|
||||
|
||||
result := pipeline()
|
||||
|
||||
assert.True(t, mock.closed, "resource should be closed in pipeline")
|
||||
assert.Equal(t, mock, result, "should return the resource")
|
||||
})
|
||||
|
||||
t.Run("works with ChainFirst", func(t *testing.T) {
|
||||
mock := &mockCloser{}
|
||||
data := "test data"
|
||||
|
||||
// Process data and close resource as side effect
|
||||
pipeline := IO.ChainFirst(func(string) IO.IO[*mockCloser] {
|
||||
return Close(mock)
|
||||
})(IO.Of(data))
|
||||
|
||||
result := pipeline()
|
||||
|
||||
assert.True(t, mock.closed, "resource should be closed")
|
||||
assert.Equal(t, data, result, "should return original data")
|
||||
})
|
||||
}
|
||||
|
||||
// TestRemove_BasicOperation tests basic Remove functionality
|
||||
func TestRemove_BasicOperation(t *testing.T) {
|
||||
t.Run("removes existing file", func(t *testing.T) {
|
||||
// Create a temporary file
|
||||
tmpFile, err := os.CreateTemp("", "test-remove-*.txt")
|
||||
require.NoError(t, err)
|
||||
tmpPath := tmpFile.Name()
|
||||
tmpFile.Close()
|
||||
|
||||
// Verify file exists
|
||||
_, err = os.Stat(tmpPath)
|
||||
require.NoError(t, err, "file should exist before removal")
|
||||
|
||||
// Remove using our function
|
||||
removeIO := Remove(tmpPath)
|
||||
result := removeIO()
|
||||
|
||||
assert.Equal(t, tmpPath, result, "should return the file path")
|
||||
|
||||
// Verify file is removed
|
||||
_, err = os.Stat(tmpPath)
|
||||
assert.True(t, os.IsNotExist(err), "file should not exist after removal")
|
||||
})
|
||||
|
||||
t.Run("ignores error for non-existent file", func(t *testing.T) {
|
||||
nonExistentPath := filepath.Join(os.TempDir(), "non-existent-file-12345.txt")
|
||||
|
||||
// Should not panic even if file doesn't exist
|
||||
removeIO := Remove(nonExistentPath)
|
||||
result := removeIO()
|
||||
|
||||
assert.Equal(t, nonExistentPath, result, "should return the path")
|
||||
})
|
||||
|
||||
t.Run("removes empty directory", func(t *testing.T) {
|
||||
// Create a temporary directory
|
||||
tmpDir, err := os.MkdirTemp("", "test-remove-dir-*")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify directory exists
|
||||
_, err = os.Stat(tmpDir)
|
||||
require.NoError(t, err, "directory should exist before removal")
|
||||
|
||||
// Remove using our function
|
||||
removeIO := Remove(tmpDir)
|
||||
result := removeIO()
|
||||
|
||||
assert.Equal(t, tmpDir, result, "should return the directory path")
|
||||
|
||||
// Verify directory is removed
|
||||
_, err = os.Stat(tmpDir)
|
||||
assert.True(t, os.IsNotExist(err), "directory should not exist after removal")
|
||||
})
|
||||
|
||||
t.Run("ignores error for non-empty directory", func(t *testing.T) {
|
||||
// Create a temporary directory with a file
|
||||
tmpDir, err := os.MkdirTemp("", "test-remove-nonempty-*")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tmpDir) // Cleanup
|
||||
|
||||
tmpFile := filepath.Join(tmpDir, "file.txt")
|
||||
err = os.WriteFile(tmpFile, []byte("data"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should not panic even if directory is not empty
|
||||
removeIO := Remove(tmpDir)
|
||||
result := removeIO()
|
||||
|
||||
assert.Equal(t, tmpDir, result, "should return the path")
|
||||
|
||||
// Directory should still exist (os.Remove doesn't remove non-empty dirs)
|
||||
_, err = os.Stat(tmpDir)
|
||||
assert.NoError(t, err, "non-empty directory should still exist")
|
||||
})
|
||||
}
|
||||
|
||||
// TestRemove_Composition tests Remove in IO composition
|
||||
func TestRemove_Composition(t *testing.T) {
|
||||
t.Run("composes with other IO operations", func(t *testing.T) {
|
||||
// Create a temporary file
|
||||
tmpFile, err := os.CreateTemp("", "test-compose-*.txt")
|
||||
require.NoError(t, err)
|
||||
tmpPath := tmpFile.Name()
|
||||
tmpFile.Close()
|
||||
|
||||
// Create a pipeline that processes and removes the file
|
||||
step1 := IO.Of(tmpPath)
|
||||
step2 := IO.Map(func(path string) string {
|
||||
// Simulate processing
|
||||
return path
|
||||
})(step1)
|
||||
pipeline := IO.Chain(Remove)(step2)
|
||||
|
||||
result := pipeline()
|
||||
|
||||
assert.Equal(t, tmpPath, result, "should return the path")
|
||||
|
||||
// Verify file is removed
|
||||
_, err = os.Stat(tmpPath)
|
||||
assert.True(t, os.IsNotExist(err), "file should be removed")
|
||||
})
|
||||
|
||||
t.Run("removes multiple files in sequence", func(t *testing.T) {
|
||||
// Create temporary files
|
||||
tmpFile1, err := os.CreateTemp("", "test-multi-1-*.txt")
|
||||
require.NoError(t, err)
|
||||
tmpPath1 := tmpFile1.Name()
|
||||
tmpFile1.Close()
|
||||
|
||||
tmpFile2, err := os.CreateTemp("", "test-multi-2-*.txt")
|
||||
require.NoError(t, err)
|
||||
tmpPath2 := tmpFile2.Name()
|
||||
tmpFile2.Close()
|
||||
|
||||
// Remove both files in sequence
|
||||
pipeline := IO.ChainTo[string](Remove(tmpPath2))(Remove(tmpPath1))
|
||||
|
||||
result := pipeline()
|
||||
|
||||
assert.Equal(t, tmpPath2, result, "should return last path")
|
||||
|
||||
// Verify both files are removed
|
||||
_, err = os.Stat(tmpPath1)
|
||||
assert.True(t, os.IsNotExist(err), "first file should be removed")
|
||||
|
||||
_, err = os.Stat(tmpPath2)
|
||||
assert.True(t, os.IsNotExist(err), "second file should be removed")
|
||||
})
|
||||
}
|
||||
|
||||
// TestRemove_CanBeCalledMultipleTimes tests idempotency
|
||||
func TestRemove_CanBeCalledMultipleTimes(t *testing.T) {
|
||||
t.Run("calling remove multiple times is safe", func(t *testing.T) {
|
||||
// Create a temporary file
|
||||
tmpFile, err := os.CreateTemp("", "test-idempotent-*.txt")
|
||||
require.NoError(t, err)
|
||||
tmpPath := tmpFile.Name()
|
||||
tmpFile.Close()
|
||||
|
||||
removeIO := Remove(tmpPath)
|
||||
|
||||
// First call removes the file
|
||||
result1 := removeIO()
|
||||
assert.Equal(t, tmpPath, result1)
|
||||
|
||||
// Second call should not panic (file already removed)
|
||||
result2 := removeIO()
|
||||
assert.Equal(t, tmpPath, result2)
|
||||
|
||||
// Verify file is removed
|
||||
_, err = os.Stat(tmpPath)
|
||||
assert.True(t, os.IsNotExist(err), "file should be removed")
|
||||
})
|
||||
}
|
||||
|
||||
// TestCloseAndRemove_Together tests using both functions together
|
||||
func TestCloseAndRemove_Together(t *testing.T) {
|
||||
t.Run("close and remove file in sequence", func(t *testing.T) {
|
||||
// Create a temporary file
|
||||
tmpFile, err := os.CreateTemp("", "test-close-remove-*.txt")
|
||||
require.NoError(t, err)
|
||||
tmpPath := tmpFile.Name()
|
||||
|
||||
// Write some data
|
||||
_, err = tmpFile.WriteString("test data")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Close and remove in sequence
|
||||
pipeline := IO.Chain(func(f *os.File) IO.IO[string] {
|
||||
return Remove(f.Name())
|
||||
})(Close(tmpFile))
|
||||
|
||||
result := pipeline()
|
||||
|
||||
assert.Equal(t, tmpPath, result, "should return the path")
|
||||
|
||||
// Verify file is removed
|
||||
_, err = os.Stat(tmpPath)
|
||||
assert.True(t, os.IsNotExist(err), "file should be removed")
|
||||
})
|
||||
}
|
||||
|
||||
// TestClose_TypeSafety tests that Close works with different io.Closer types
|
||||
func TestClose_TypeSafety(t *testing.T) {
|
||||
t.Run("works with different closer types", func(t *testing.T) {
|
||||
// Test with different types that implement io.Closer
|
||||
types := []io.Closer{
|
||||
&mockCloser{},
|
||||
io.NopCloser(bytes.NewBuffer(nil)),
|
||||
}
|
||||
|
||||
for _, closer := range types {
|
||||
closeIO := Close(closer)
|
||||
result := closeIO()
|
||||
assert.Equal(t, closer, result, "should return the same closer")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Example_close demonstrates basic usage of Close
|
||||
func Example_close() {
|
||||
// Create a mock closer
|
||||
mock := &mockCloser{}
|
||||
|
||||
// Create an IO that closes the resource
|
||||
closeIO := Close(mock)
|
||||
|
||||
// Execute the IO
|
||||
result := closeIO()
|
||||
|
||||
fmt.Printf("Closed: %v\n", result.closed)
|
||||
// Output: Closed: true
|
||||
}
|
||||
|
||||
// Example_remove demonstrates basic usage of Remove
|
||||
func Example_remove() {
|
||||
// Create a temporary file
|
||||
tmpFile, _ := os.CreateTemp("", "example-*.txt")
|
||||
tmpPath := tmpFile.Name()
|
||||
tmpFile.Close()
|
||||
|
||||
// Create an IO that removes the file
|
||||
removeIO := Remove(tmpPath)
|
||||
|
||||
// Execute the IO
|
||||
path := removeIO()
|
||||
|
||||
// Check if file exists
|
||||
_, err := os.Stat(path)
|
||||
fmt.Printf("File removed: %v\n", os.IsNotExist(err))
|
||||
// Output: File removed: true
|
||||
}
|
||||
|
||||
// Example_closeAndRemove demonstrates using Close and Remove together
|
||||
func Example_closeAndRemove() {
|
||||
// Create a temporary file
|
||||
tmpFile, _ := os.CreateTemp("", "example-*.txt")
|
||||
|
||||
// Create a pipeline that closes and removes the file
|
||||
pipeline := IO.Chain(func(f *os.File) IO.IO[string] {
|
||||
return Remove(f.Name())
|
||||
})(Close(tmpFile))
|
||||
|
||||
// Execute the pipeline
|
||||
path := pipeline()
|
||||
|
||||
// Check if file exists
|
||||
_, err := os.Stat(path)
|
||||
fmt.Printf("File removed: %v\n", os.IsNotExist(err))
|
||||
// Output: File removed: true
|
||||
}
|
||||
262
v2/optics/codec/codec.go
Normal file
262
v2/optics/codec/codec.go
Normal file
@@ -0,0 +1,262 @@
|
||||
package codec
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
"github.com/IBM/fp-go/v2/array"
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readereither"
|
||||
R "github.com/IBM/fp-go/v2/reflect"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// typeImpl is the internal implementation of the Type interface.
|
||||
// It combines encoding, decoding, validation, and type checking capabilities.
|
||||
type typeImpl[A, O, I any] struct {
|
||||
name string
|
||||
is Reader[any, Result[A]]
|
||||
validate Validate[I, A]
|
||||
encode Encode[A, O]
|
||||
}
|
||||
|
||||
// MakeType creates a new Type with the given name, type checker, validator, and encoder.
|
||||
//
|
||||
// Parameters:
|
||||
// - name: A descriptive name for this type (used in error messages)
|
||||
// - is: A function that checks if a value is of type A
|
||||
// - validate: A function that validates and decodes input I to type A
|
||||
// - encode: A function that encodes type A to output O
|
||||
//
|
||||
// Returns a Type[A, O, I] that can both encode and decode values.
|
||||
func MakeType[A, O, I any](
|
||||
name string,
|
||||
is Reader[any, Result[A]],
|
||||
validate Validate[I, A],
|
||||
encode Encode[A, O],
|
||||
) Type[A, O, I] {
|
||||
return &typeImpl[A, O, I]{
|
||||
name: name,
|
||||
is: is,
|
||||
validate: validate,
|
||||
encode: encode,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate validates the input value in the context of a validation path.
|
||||
// Returns a Reader that takes a Context and produces a Validation result.
|
||||
func (t *typeImpl[A, O, I]) Validate(i I) Reader[Context, Validation[A]] {
|
||||
return t.validate(i)
|
||||
}
|
||||
|
||||
// Decode validates and decodes the input value, creating a new context with this type's name.
|
||||
// This is a convenience method that calls Validate with a fresh context.
|
||||
func (t *typeImpl[A, O, I]) Decode(i I) Validation[A] {
|
||||
return t.validate(i)(array.Of(validation.ContextEntry{Type: t.name, Actual: i}))
|
||||
}
|
||||
|
||||
// Encode transforms a value of type A into the output format O.
|
||||
func (t *typeImpl[A, O, I]) Encode(a A) O {
|
||||
return t.encode(a)
|
||||
}
|
||||
|
||||
// AsDecoder returns this Type as a Decoder interface.
|
||||
func (t *typeImpl[A, O, I]) AsDecoder() Decoder[I, A] {
|
||||
return t
|
||||
}
|
||||
|
||||
// AsEncoder returns this Type as an Encoder interface.
|
||||
func (t *typeImpl[A, O, I]) AsEncoder() Encoder[A, O] {
|
||||
return t
|
||||
}
|
||||
|
||||
// Name returns the descriptive name of this type.
|
||||
func (t *typeImpl[A, O, I]) Name() string {
|
||||
return t.name
|
||||
}
|
||||
|
||||
func (t *typeImpl[A, O, I]) Is(i any) Result[A] {
|
||||
return t.is(i)
|
||||
}
|
||||
|
||||
// Pipe composes two Types, creating a pipeline where:
|
||||
// - Decoding: I -> A -> B (decode with 'this', then validate with 'ab')
|
||||
// - Encoding: B -> A -> O (encode with 'ab', then encode with 'this')
|
||||
//
|
||||
// This allows building complex codecs from simpler ones.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// stringToInt := codec.MakeType(...) // Type[int, string, string]
|
||||
// intToPositive := codec.MakeType(...) // Type[PositiveInt, int, int]
|
||||
// composed := codec.Pipe(intToPositive)(stringToInt) // Type[PositiveInt, string, string]
|
||||
func Pipe[A, B, O, I any](ab Type[B, A, A]) func(Type[A, O, I]) Type[B, O, I] {
|
||||
return func(this Type[A, O, I]) Type[B, O, I] {
|
||||
return MakeType(
|
||||
fmt.Sprintf("Pipe(%s, %s)", this.Name(), ab.Name()),
|
||||
ab.Is,
|
||||
F.Flow2(
|
||||
this.Validate,
|
||||
readereither.Chain(ab.Validate),
|
||||
),
|
||||
F.Flow2(
|
||||
ab.Encode,
|
||||
this.Encode,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// isNil checks if a value is nil, handling both typed and untyped nil values.
|
||||
// It uses reflection to detect nil pointers, maps, slices, channels, functions, and interfaces.
|
||||
func isNil(x any) bool {
|
||||
if x == nil {
|
||||
return true
|
||||
}
|
||||
v := reflect.ValueOf(x)
|
||||
switch v.Kind() {
|
||||
case reflect.Ptr, reflect.Map, reflect.Slice, reflect.Chan, reflect.Func, reflect.Interface:
|
||||
return v.IsNil()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// isTypedNil checks if a value is nil and returns it as a typed nil pointer.
|
||||
// Returns Some(nil) if the value is nil, None otherwise.
|
||||
func isTypedNil[A any](x any) Result[*A] {
|
||||
if isNil(x) {
|
||||
return result.Of[*A](nil)
|
||||
}
|
||||
return result.Left[*A](errors.New("expecting nil"))
|
||||
}
|
||||
|
||||
func validateFromIs[A any](
|
||||
is ReaderResult[any, A],
|
||||
msg string,
|
||||
) Reader[any, Reader[Context, Validation[A]]] {
|
||||
return func(u any) Reader[Context, Validation[A]] {
|
||||
return F.Pipe2(
|
||||
u,
|
||||
is,
|
||||
result.Fold(
|
||||
validation.FailureWithError[A](u, msg),
|
||||
F.Flow2(
|
||||
validation.Success[A],
|
||||
reader.Of[Context],
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MakeNilType creates a Type that validates nil values.
|
||||
// It accepts any input and validates that it is nil, returning a typed nil pointer.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// nilType := codec.MakeNilType[string]()
|
||||
// result := nilType.Decode(nil) // Success: Right((*string)(nil))
|
||||
// result := nilType.Decode("not nil") // Failure: Left(errors)
|
||||
func Nil[A any]() Type[*A, *A, any] {
|
||||
|
||||
is := isTypedNil[A]
|
||||
|
||||
return MakeType(
|
||||
"nil",
|
||||
is,
|
||||
validateFromIs(is, "nil"),
|
||||
F.Identity[*A],
|
||||
)
|
||||
}
|
||||
|
||||
func MakeSimpleType[A any]() Type[A, A, any] {
|
||||
var zero A
|
||||
name := fmt.Sprintf("%T", zero)
|
||||
is := Is[A]()
|
||||
|
||||
return MakeType(
|
||||
name,
|
||||
is,
|
||||
validateFromIs(is, name),
|
||||
F.Identity[A],
|
||||
)
|
||||
}
|
||||
|
||||
func String() Type[string, string, any] {
|
||||
return MakeSimpleType[string]()
|
||||
}
|
||||
|
||||
func Int() Type[int, int, any] {
|
||||
return MakeSimpleType[int]()
|
||||
}
|
||||
|
||||
func Bool() Type[bool, bool, any] {
|
||||
return MakeSimpleType[bool]()
|
||||
}
|
||||
|
||||
func appendContext(key, typ string, actual any) Endomorphism[Context] {
|
||||
return A.Push(validation.ContextEntry{Key: key, Type: typ, Actual: actual})
|
||||
}
|
||||
|
||||
type validationPair[T any] = Pair[validation.Errors, T]
|
||||
|
||||
func pairToValidation[T any](p validationPair[T]) Validation[T] {
|
||||
errors, value := pair.Unpack(p)
|
||||
if A.IsNonEmpty(errors) {
|
||||
return either.Left[T](errors)
|
||||
}
|
||||
return either.Of[validation.Errors](value)
|
||||
}
|
||||
|
||||
func validateArray[T any](item Type[T, T, any]) func(u any) Reader[Context, Validation[[]T]] {
|
||||
|
||||
appendErrors := F.Flow2(
|
||||
A.Concat,
|
||||
pair.MapHead[[]T, validation.Errors],
|
||||
)
|
||||
|
||||
appendValues := F.Flow2(
|
||||
A.Push,
|
||||
pair.MapTail[validation.Errors, []T],
|
||||
)
|
||||
|
||||
itemName := item.Name()
|
||||
|
||||
zero := pair.Zero[validation.Errors, []T]()
|
||||
|
||||
return func(u any) Reader[Context, Validation[[]T]] {
|
||||
val := reflect.ValueOf(u)
|
||||
if !val.IsValid() {
|
||||
return validation.FailureWithMessage[[]T](val, "invalid value")
|
||||
}
|
||||
kind := val.Kind()
|
||||
|
||||
switch kind {
|
||||
case reflect.Array, reflect.Slice, reflect.String:
|
||||
|
||||
return func(c Context) Validation[[]T] {
|
||||
|
||||
return F.Pipe1(
|
||||
R.MonadReduceWithIndex(val, func(i int, p validationPair[[]T], v reflect.Value) validationPair[[]T] {
|
||||
return either.MonadFold(
|
||||
item.Validate(v)(appendContext(strconv.Itoa(i), itemName, v)(c)),
|
||||
appendErrors,
|
||||
appendValues,
|
||||
)(p)
|
||||
}, zero),
|
||||
pairToValidation,
|
||||
)
|
||||
}
|
||||
default:
|
||||
return validation.FailureWithMessage[[]T](val, fmt.Sprintf("type %s is not iterable", kind))
|
||||
}
|
||||
}
|
||||
}
|
||||
15
v2/optics/codec/codec_test.go
Normal file
15
v2/optics/codec/codec_test.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package codec
|
||||
|
||||
import (
|
||||
"log"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStringCoded(t *testing.T) {
|
||||
|
||||
sType := String()
|
||||
|
||||
res := sType.Decode(10)
|
||||
|
||||
log.Println(res)
|
||||
}
|
||||
57
v2/optics/codec/doc.go
Normal file
57
v2/optics/codec/doc.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// Package codec provides a functional approach to encoding and decoding data with validation.
|
||||
//
|
||||
// The codec package combines the concepts of encoders and decoders into a unified Type that can
|
||||
// both encode values to an output format and decode/validate values from an input format. This
|
||||
// is particularly useful for data serialization, API validation, and type-safe transformations.
|
||||
//
|
||||
// # Core Concepts
|
||||
//
|
||||
// Type[A, O, I]: A bidirectional codec that can:
|
||||
// - Decode input I to type A with validation
|
||||
// - Encode type A to output O
|
||||
// - Check if a value is of type A
|
||||
//
|
||||
// Validation: Decoding returns Either[Errors, A] which represents:
|
||||
// - Left(Errors): Validation failed with detailed error information
|
||||
// - Right(A): Successfully decoded and validated value
|
||||
//
|
||||
// Context: A stack of ContextEntry values that tracks the path through nested structures
|
||||
// during validation, providing detailed error messages.
|
||||
//
|
||||
// # Basic Usage
|
||||
//
|
||||
// Creating a simple type:
|
||||
//
|
||||
// nilType := codec.MakeNilType[string]()
|
||||
// result := nilType.Decode(nil) // Success
|
||||
// result := nilType.Decode("not nil") // Failure
|
||||
//
|
||||
// Composing types with Pipe:
|
||||
//
|
||||
// composed := codec.Pipe(typeB)(typeA)
|
||||
// // Decodes: I -> A -> B
|
||||
// // Encodes: B -> A -> O
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// Most functions use three type parameters:
|
||||
// - A: The domain type (the actual Go type being encoded/decoded)
|
||||
// - O: The output type for encoding
|
||||
// - I: The input type for decoding
|
||||
//
|
||||
// # Validation Errors
|
||||
//
|
||||
// ValidationError contains:
|
||||
// - Value: The actual value that failed validation
|
||||
// - Context: The path to the value in nested structures
|
||||
// - Message: Human-readable error description
|
||||
//
|
||||
// # Integration
|
||||
//
|
||||
// This package integrates with:
|
||||
// - optics/decoder: For decoding operations
|
||||
// - optics/encoder: For encoding operations
|
||||
// - either: For validation results
|
||||
// - option: For optional type checking
|
||||
// - reader: For context-dependent operations
|
||||
package codec
|
||||
83
v2/optics/codec/types.go
Normal file
83
v2/optics/codec/types.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package codec
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/optics/decoder"
|
||||
"github.com/IBM/fp-go/v2/optics/encoder"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerresult"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
type (
|
||||
ReaderResult[R, A any] = readerresult.ReaderResult[R, A]
|
||||
|
||||
// Lazy represents a lazily evaluated value.
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
|
||||
// Reader represents a computation that depends on an environment R and produces a value A.
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
|
||||
// Option represents an optional value that may or may not be present.
|
||||
Option[A any] = option.Option[A]
|
||||
|
||||
// Either represents a value that can be one of two types: Left (error) or Right (success).
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
|
||||
// Result represents a computation that may fail with an error.
|
||||
Result[A any] = result.Result[A]
|
||||
|
||||
// Codec combines a Decoder and an Encoder for bidirectional transformations.
|
||||
// It can decode input I to type A and encode type A to output O.
|
||||
Codec[I, O, A any] struct {
|
||||
Decode decoder.Decoder[I, A]
|
||||
Encode encoder.Encoder[O, A]
|
||||
}
|
||||
|
||||
Validation[A any] = validation.Validation[A]
|
||||
|
||||
Context = validation.Context
|
||||
|
||||
// Validate is a function that validates input I to produce type A.
|
||||
// It takes an input and returns a Reader that depends on the validation Context.
|
||||
Validate[I, A any] = Reader[I, Reader[Context, Validation[A]]]
|
||||
|
||||
// Decode is a function that decodes input I to type A with validation.
|
||||
// It returns a Validation result directly.
|
||||
Decode[I, A any] = Reader[I, Validation[A]]
|
||||
|
||||
// Encode is a function that encodes type A to output O.
|
||||
Encode[A, O any] = Reader[A, O]
|
||||
|
||||
// Decoder is an interface for types that can decode and validate input.
|
||||
Decoder[I, A any] interface {
|
||||
Name() string
|
||||
Validate(I) Reader[Context, Validation[A]]
|
||||
Decode(I) Validation[A]
|
||||
}
|
||||
|
||||
// Encoder is an interface for types that can encode values.
|
||||
Encoder[A, O any] interface {
|
||||
// Encode transforms a value of type A into output format O.
|
||||
Encode(A) O
|
||||
}
|
||||
// Type is a bidirectional codec that combines encoding, decoding, validation,
|
||||
// and type checking capabilities. It represents a complete specification of
|
||||
// how to work with a particular type.
|
||||
Type[A, O, I any] interface {
|
||||
Decoder[I, A]
|
||||
Encoder[A, O]
|
||||
AsDecoder() Decoder[I, A]
|
||||
AsEncoder() Encoder[A, O]
|
||||
Is(any) Result[A]
|
||||
}
|
||||
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
Pair[L, R any] = pair.Pair[L, R]
|
||||
)
|
||||
21
v2/optics/codec/validation.go
Normal file
21
v2/optics/codec/validation.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package codec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
func onTypeError(expType string) func(any) error {
|
||||
return func(u any) error {
|
||||
return fmt.Errorf("expecting type [%s] but got [%T]", expType, u)
|
||||
}
|
||||
}
|
||||
|
||||
// Is checks if a value can be converted to type T.
|
||||
// Returns Some(value) if the conversion succeeds, None otherwise.
|
||||
// This is a type-safe cast operation.
|
||||
func Is[T any]() func(any) Result[T] {
|
||||
var zero T
|
||||
return result.ToType[T](onTypeError(fmt.Sprintf("%T", zero)))
|
||||
}
|
||||
104
v2/optics/codec/validation/doc.go
Normal file
104
v2/optics/codec/validation/doc.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// Package validation provides functional validation types and operations for the codec system.
|
||||
//
|
||||
// This package implements a validation monad that accumulates errors during validation operations,
|
||||
// making it ideal for form validation, data parsing, and other scenarios where you want to collect
|
||||
// all validation errors rather than failing on the first error.
|
||||
//
|
||||
// # Core Concepts
|
||||
//
|
||||
// Validation[A]: Represents the result of a validation operation as Either[Errors, A]:
|
||||
// - Left(Errors): Validation failed with one or more errors
|
||||
// - Right(A): Successfully validated value of type A
|
||||
//
|
||||
// ValidationError: A detailed error type that includes:
|
||||
// - Value: The actual value that failed validation
|
||||
// - Context: The path through nested structures (e.g., "user.address.zipCode")
|
||||
// - Message: Human-readable error description
|
||||
// - Cause: Optional underlying error
|
||||
//
|
||||
// Context: A stack of ContextEntry values that tracks the validation path through
|
||||
// nested data structures, enabling precise error reporting.
|
||||
//
|
||||
// # Basic Usage
|
||||
//
|
||||
// Creating validation results:
|
||||
//
|
||||
// // Success case
|
||||
// valid := validation.Success(42)
|
||||
//
|
||||
// // Failure case
|
||||
// invalid := validation.Failures[int](validation.Errors{
|
||||
// &validation.ValidationError{
|
||||
// Value: "not a number",
|
||||
// Message: "expected integer",
|
||||
// Context: nil,
|
||||
// },
|
||||
// })
|
||||
//
|
||||
// Using with context:
|
||||
//
|
||||
// failWithMsg := validation.FailureWithMessage[int]("invalid", "must be positive")
|
||||
// result := failWithMsg([]validation.ContextEntry{
|
||||
// {Key: "age", Type: "int"},
|
||||
// })
|
||||
//
|
||||
// # Applicative Validation
|
||||
//
|
||||
// The validation type supports applicative operations, allowing you to combine
|
||||
// multiple validations and accumulate all errors:
|
||||
//
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Email string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// validateName := func(s string) validation.Validation[string] {
|
||||
// if len(s) > 0 {
|
||||
// return validation.Success(s)
|
||||
// }
|
||||
// return validation.Failures[string](/* error */)
|
||||
// }
|
||||
//
|
||||
// // Combine validations - all errors will be collected
|
||||
// result := validation.Ap(validation.Ap(validation.Ap(
|
||||
// validation.Of(func(name string) func(email string) func(age int) User {
|
||||
// return func(email string) func(age int) User {
|
||||
// return func(age int) User {
|
||||
// return User{name, email, age}
|
||||
// }
|
||||
// }
|
||||
// }),
|
||||
// )(validateName("")))(validateEmail("")))(validateAge(-1))
|
||||
//
|
||||
// # Error Formatting
|
||||
//
|
||||
// ValidationError implements custom formatting for detailed error messages:
|
||||
//
|
||||
// err := &ValidationError{
|
||||
// Value: "abc",
|
||||
// Context: []ContextEntry{{Key: "user"}, {Key: "age"}},
|
||||
// Message: "expected integer",
|
||||
// }
|
||||
//
|
||||
// fmt.Printf("%v", err) // at user.age: expected integer
|
||||
// fmt.Printf("%+v", err) // at user.age: expected integer
|
||||
// // value: "abc"
|
||||
//
|
||||
// # Monoid Operations
|
||||
//
|
||||
// The package provides monoid instances for combining validations:
|
||||
//
|
||||
// // Combine validation results
|
||||
// m := validation.ApplicativeMonoid(stringMonoid)
|
||||
// combined := m.Concat(validation.Success("hello"), validation.Success(" world"))
|
||||
// // Result: Success("hello world")
|
||||
//
|
||||
// # Integration
|
||||
//
|
||||
// This package integrates with:
|
||||
// - either: Validation is built on Either for error handling
|
||||
// - array: For collecting multiple errors
|
||||
// - monoid: For combining validation results
|
||||
// - reader: For context-dependent validation operations
|
||||
package validation
|
||||
112
v2/optics/codec/validation/monad.go
Normal file
112
v2/optics/codec/validation/monad.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/internal/applicative"
|
||||
)
|
||||
|
||||
// Of creates a successful validation result containing the given value.
|
||||
// This is the pure/return operation for the Validation monad.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// valid := Of(42) // Validation[int] containing 42
|
||||
func Of[A any](a A) Validation[A] {
|
||||
return either.Of[Errors](a)
|
||||
}
|
||||
|
||||
// Ap applies a validation containing a function to a validation containing a value.
|
||||
// This is the applicative apply operation that accumulates errors from both validations.
|
||||
// If either validation fails, all errors are collected. If both succeed, the function is applied.
|
||||
//
|
||||
// This enables combining multiple validations while collecting all errors:
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Validate multiple fields and collect all errors
|
||||
// validateUser := Ap(Ap(Of(func(name string) func(age int) User {
|
||||
// return func(age int) User { return User{name, age} }
|
||||
// }))(validateName))(validateAge)
|
||||
func Ap[B, A any](fa Validation[A]) Operator[func(A) B, B] {
|
||||
return either.ApV[B, A](ErrorsMonoid())(fa)
|
||||
}
|
||||
|
||||
// Map transforms the value inside a successful validation using the provided function.
|
||||
// If the validation is a failure, the errors are preserved unchanged.
|
||||
// This is the functor map operation for Validation.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// doubled := Map(func(x int) int { return x * 2 })(Of(21))
|
||||
// // Result: Success(42)
|
||||
func Map[A, B any](f func(A) B) Operator[A, B] {
|
||||
return either.Map[Errors](f)
|
||||
}
|
||||
|
||||
// Applicative creates an Applicative instance for Validation with error accumulation.
|
||||
//
|
||||
// This returns a lawful Applicative that accumulates validation errors using the Errors monoid.
|
||||
// Unlike the standard Either applicative which fails fast, this validation applicative collects
|
||||
// all errors when combining independent validations with Ap.
|
||||
//
|
||||
// The returned instance satisfies all applicative laws:
|
||||
// - Identity: Ap(Of(identity))(v) == v
|
||||
// - Homomorphism: Ap(Of(f))(Of(x)) == Of(f(x))
|
||||
// - Interchange: Ap(Of(f))(u) == Ap(Map(f => f(y))(u))(Of(y))
|
||||
// - Composition: Ap(Ap(Map(compose)(f))(g))(x) == Ap(f)(Ap(g)(x))
|
||||
//
|
||||
// Key behaviors:
|
||||
// - Of: lifts a value into a successful Validation (Right)
|
||||
// - Map: transforms successful values, preserves failures (standard functor)
|
||||
// - Ap: when both operands fail, combines all errors using the Errors monoid
|
||||
//
|
||||
// This is particularly useful for form validation, configuration validation, and any scenario
|
||||
// where you want to collect all validation errors at once rather than stopping at the first failure.
|
||||
//
|
||||
// Example - Validating Multiple Fields:
|
||||
//
|
||||
// app := Applicative[string, User]()
|
||||
//
|
||||
// // Validate individual fields
|
||||
// validateName := func(name string) Validation[string] {
|
||||
// if len(name) < 3 {
|
||||
// return Failure("Name must be at least 3 characters")
|
||||
// }
|
||||
// return Success(name)
|
||||
// }
|
||||
//
|
||||
// validateAge := func(age int) Validation[int] {
|
||||
// if age < 18 {
|
||||
// return Failure("Must be 18 or older")
|
||||
// }
|
||||
// return Success(age)
|
||||
// }
|
||||
//
|
||||
// // Create a curried constructor
|
||||
// makeUser := func(name string) func(int) User {
|
||||
// return func(age int) User {
|
||||
// return User{Name: name, Age: age}
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Combine validations - all errors are collected
|
||||
// name := validateName("ab") // Failure: name too short
|
||||
// age := validateAge(16) // Failure: age too low
|
||||
//
|
||||
// result := app.Ap(age)(app.Ap(name)(app.Of(makeUser)))
|
||||
// // result contains both validation errors:
|
||||
// // - "Name must be at least 3 characters"
|
||||
// // - "Must be 18 or older"
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The input value type (Right value)
|
||||
// - B: The output value type after transformation
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// An Applicative instance with Of, Map, and Ap operations that accumulate errors
|
||||
func Applicative[A, B any]() applicative.Applicative[A, B, Validation[A], Validation[B], Validation[func(A) B]] {
|
||||
return either.ApplicativeV[Errors, A, B](
|
||||
ErrorsMonoid(),
|
||||
)
|
||||
}
|
||||
921
v2/optics/codec/validation/monad_test.go
Normal file
921
v2/optics/codec/validation/monad_test.go
Normal file
@@ -0,0 +1,921 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOf(t *testing.T) {
|
||||
t.Run("creates successful validation", func(t *testing.T) {
|
||||
result := Of(42)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
strResult := Of("hello")
|
||||
assert.True(t, either.IsRight(strResult))
|
||||
|
||||
boolResult := Of(true)
|
||||
assert.True(t, either.IsRight(boolResult))
|
||||
|
||||
type Custom struct{ Value int }
|
||||
customResult := Of(Custom{Value: 100})
|
||||
assert.True(t, either.IsRight(customResult))
|
||||
})
|
||||
|
||||
t.Run("is equivalent to Success", func(t *testing.T) {
|
||||
value := 42
|
||||
ofResult := Of(value)
|
||||
successResult := Success(value)
|
||||
|
||||
assert.Equal(t, ofResult, successResult)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
t.Run("transforms successful validation", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
result := Map(double)(Of(21))
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
errs := Errors{&ValidationError{Messsage: "error"}}
|
||||
failure := Failures[int](errs)
|
||||
|
||||
double := func(x int) int { return x * 2 }
|
||||
result := Map(double)(failure)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("chains multiple maps", func(t *testing.T) {
|
||||
add10 := func(x int) int { return x + 10 }
|
||||
double := func(x int) int { return x * 2 }
|
||||
toString := func(x int) string { return fmt.Sprintf("%d", x) }
|
||||
|
||||
result := F.Pipe3(
|
||||
Of(5),
|
||||
Map(add10),
|
||||
Map(double),
|
||||
Map(toString),
|
||||
)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "30", value)
|
||||
})
|
||||
|
||||
t.Run("type transformation", func(t *testing.T) {
|
||||
length := func(s string) int { return len(s) }
|
||||
result := Map(length)(Of("hello"))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 5, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAp(t *testing.T) {
|
||||
t.Run("applies function to value when both succeed", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
funcValidation := Of(double)
|
||||
valueValidation := Of(21)
|
||||
|
||||
result := Ap[int, int](valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("accumulates errors when value fails", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
funcValidation := Of(double)
|
||||
valueValidation := Failures[int](Errors{
|
||||
&ValidationError{Messsage: "value error"},
|
||||
})
|
||||
|
||||
result := Ap[int, int](valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "value error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("accumulates errors when function fails", func(t *testing.T) {
|
||||
funcValidation := Failures[func(int) int](Errors{
|
||||
&ValidationError{Messsage: "function error"},
|
||||
})
|
||||
valueValidation := Of(21)
|
||||
|
||||
result := Ap[int, int](valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "function error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("accumulates all errors when both fail", func(t *testing.T) {
|
||||
funcValidation := Failures[func(int) int](Errors{
|
||||
&ValidationError{Messsage: "function error"},
|
||||
})
|
||||
valueValidation := Failures[int](Errors{
|
||||
&ValidationError{Messsage: "value error"},
|
||||
})
|
||||
|
||||
result := Ap[int, int](valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2)
|
||||
messages := []string{errors[0].Messsage, errors[1].Messsage}
|
||||
assert.Contains(t, messages, "function error")
|
||||
assert.Contains(t, messages, "value error")
|
||||
})
|
||||
|
||||
t.Run("applies with string transformation", func(t *testing.T) {
|
||||
toUpper := func(s string) string { return fmt.Sprintf("UPPER:%s", s) }
|
||||
funcValidation := Of(toUpper)
|
||||
valueValidation := Of("hello")
|
||||
|
||||
result := Ap[string, string](valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "UPPER:hello", value)
|
||||
})
|
||||
|
||||
t.Run("accumulates multiple validation errors from different sources", func(t *testing.T) {
|
||||
funcValidation := Failures[func(int) int](Errors{
|
||||
&ValidationError{Messsage: "function error 1"},
|
||||
&ValidationError{Messsage: "function error 2"},
|
||||
})
|
||||
valueValidation := Failures[int](Errors{
|
||||
&ValidationError{Messsage: "value error 1"},
|
||||
})
|
||||
|
||||
result := Ap[int, int](valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 3)
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "function error 1")
|
||||
assert.Contains(t, messages, "function error 2")
|
||||
assert.Contains(t, messages, "value error 1")
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadLaws(t *testing.T) {
|
||||
t.Run("functor identity law", func(t *testing.T) {
|
||||
// Map(id) == id
|
||||
value := Of(42)
|
||||
mapped := Map(F.Identity[int])(value)
|
||||
|
||||
assert.Equal(t, value, mapped)
|
||||
})
|
||||
|
||||
t.Run("functor composition law", func(t *testing.T) {
|
||||
// Map(f . g) == Map(f) . Map(g)
|
||||
f := func(x int) int { return x * 2 }
|
||||
g := func(x int) int { return x + 10 }
|
||||
composed := func(x int) int { return f(g(x)) }
|
||||
|
||||
value := Of(5)
|
||||
left := Map(composed)(value)
|
||||
right := F.Pipe2(value, Map(g), Map(f))
|
||||
|
||||
assert.Equal(t, left, right)
|
||||
})
|
||||
|
||||
t.Run("applicative identity law", func(t *testing.T) {
|
||||
// Ap(v)(Of(id)) == v
|
||||
v := Of(42)
|
||||
result := Ap[int, int](v)(Of(F.Identity[int]))
|
||||
|
||||
assert.Equal(t, v, result)
|
||||
})
|
||||
|
||||
t.Run("applicative homomorphism law", func(t *testing.T) {
|
||||
// Ap(Of(x))(Of(f)) == Of(f(x))
|
||||
f := func(x int) int { return x * 2 }
|
||||
x := 21
|
||||
|
||||
left := Ap[int, int](Of(x))(Of(f))
|
||||
right := Of(f(x))
|
||||
|
||||
assert.Equal(t, left, right)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMapWithOperator(t *testing.T) {
|
||||
t.Run("Map returns an Operator", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
operator := Map(double)
|
||||
|
||||
// Operator can be applied to different validations
|
||||
result1 := operator(Of(10))
|
||||
result2 := operator(Of(20))
|
||||
|
||||
val1 := either.MonadFold(result1,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
val2 := either.MonadFold(result2,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
assert.Equal(t, 20, val1)
|
||||
assert.Equal(t, 40, val2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApWithOperator(t *testing.T) {
|
||||
t.Run("Ap returns an Operator", func(t *testing.T) {
|
||||
valueValidation := Of(21)
|
||||
operator := Ap[int, int](valueValidation)
|
||||
|
||||
// Operator can be applied to different function validations
|
||||
double := func(x int) int { return x * 2 }
|
||||
triple := func(x int) int { return x * 3 }
|
||||
|
||||
result1 := operator(Of(double))
|
||||
result2 := operator(Of(triple))
|
||||
|
||||
val1 := either.MonadFold(result1,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
val2 := either.MonadFold(result2,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
assert.Equal(t, 42, val1)
|
||||
assert.Equal(t, 63, val2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicative(t *testing.T) {
|
||||
t.Run("returns non-nil instance", func(t *testing.T) {
|
||||
app := Applicative[int, string]()
|
||||
assert.NotNil(t, app)
|
||||
})
|
||||
|
||||
t.Run("multiple calls return independent instances", func(t *testing.T) {
|
||||
app1 := Applicative[int, string]()
|
||||
app2 := Applicative[int, string]()
|
||||
|
||||
// Both should work independently
|
||||
result1 := app1.Of(42)
|
||||
result2 := app2.Of(43)
|
||||
|
||||
assert.True(t, either.IsRight(result1))
|
||||
assert.True(t, either.IsRight(result2))
|
||||
|
||||
val1 := either.MonadFold(result1, func(Errors) int { return 0 }, F.Identity[int])
|
||||
val2 := either.MonadFold(result2, func(Errors) int { return 0 }, F.Identity[int])
|
||||
|
||||
assert.Equal(t, 42, val1)
|
||||
assert.Equal(t, 43, val2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeOf(t *testing.T) {
|
||||
app := Applicative[int, string]()
|
||||
|
||||
t.Run("wraps a value in Validation context", func(t *testing.T) {
|
||||
result := app.Of(42)
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("wraps string value", func(t *testing.T) {
|
||||
app := Applicative[string, int]()
|
||||
result := app.Of("hello")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "hello", value)
|
||||
})
|
||||
|
||||
t.Run("wraps zero value", func(t *testing.T) {
|
||||
result := app.Of(0)
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return -1 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 0, value)
|
||||
})
|
||||
|
||||
t.Run("wraps complex types", func(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
app := Applicative[User, string]()
|
||||
user := User{Name: "Alice", Age: 30}
|
||||
result := app.Of(user)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) User { return User{} },
|
||||
F.Identity[User],
|
||||
)
|
||||
assert.Equal(t, user, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeMap(t *testing.T) {
|
||||
app := Applicative[int, int]()
|
||||
|
||||
t.Run("maps a function over successful validation", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
result := app.Map(double)(app.Of(21))
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("maps type conversion", func(t *testing.T) {
|
||||
app := Applicative[int, string]()
|
||||
toString := func(x int) string { return fmt.Sprintf("%d", x) }
|
||||
result := app.Map(toString)(app.Of(42))
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "42", value)
|
||||
})
|
||||
|
||||
t.Run("maps identity function", func(t *testing.T) {
|
||||
result := app.Map(F.Identity[int])(app.Of(42))
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
errs := Errors{&ValidationError{Messsage: "error"}}
|
||||
failure := Failures[int](errs)
|
||||
|
||||
double := func(x int) int { return x * 2 }
|
||||
result := app.Map(double)(failure)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("chains multiple maps", func(t *testing.T) {
|
||||
app := Applicative[int, string]()
|
||||
add10 := func(x int) int { return x + 10 }
|
||||
double := func(x int) int { return x * 2 }
|
||||
toString := func(x int) string { return fmt.Sprintf("%d", x) }
|
||||
|
||||
result := F.Pipe3(
|
||||
app.Of(5),
|
||||
Map(add10),
|
||||
Map(double),
|
||||
app.Map(toString),
|
||||
)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "30", value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeAp(t *testing.T) {
|
||||
app := Applicative[int, int]()
|
||||
|
||||
t.Run("applies wrapped function to wrapped value when both succeed", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
funcValidation := Of(double)
|
||||
valueValidation := app.Of(21)
|
||||
|
||||
result := app.Ap(valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("accumulates errors when value fails", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
funcValidation := Of(double)
|
||||
valueValidation := Failures[int](Errors{
|
||||
&ValidationError{Messsage: "value error"},
|
||||
})
|
||||
|
||||
result := app.Ap(valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "value error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("accumulates errors when function fails", func(t *testing.T) {
|
||||
funcValidation := Failures[func(int) int](Errors{
|
||||
&ValidationError{Messsage: "function error"},
|
||||
})
|
||||
valueValidation := app.Of(21)
|
||||
|
||||
result := app.Ap(valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "function error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("accumulates all errors when both fail", func(t *testing.T) {
|
||||
funcValidation := Failures[func(int) int](Errors{
|
||||
&ValidationError{Messsage: "function error"},
|
||||
})
|
||||
valueValidation := Failures[int](Errors{
|
||||
&ValidationError{Messsage: "value error"},
|
||||
})
|
||||
|
||||
result := app.Ap(valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2)
|
||||
messages := []string{errors[0].Messsage, errors[1].Messsage}
|
||||
assert.Contains(t, messages, "function error")
|
||||
assert.Contains(t, messages, "value error")
|
||||
})
|
||||
|
||||
t.Run("applies with type conversion", func(t *testing.T) {
|
||||
app := Applicative[int, string]()
|
||||
toString := func(x int) string { return fmt.Sprintf("value:%d", x) }
|
||||
funcValidation := Of(toString)
|
||||
valueValidation := app.Of(42)
|
||||
|
||||
result := app.Ap(valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "value:42", value)
|
||||
})
|
||||
|
||||
t.Run("accumulates multiple errors from different sources", func(t *testing.T) {
|
||||
funcValidation := Failures[func(int) int](Errors{
|
||||
&ValidationError{Messsage: "function error 1"},
|
||||
&ValidationError{Messsage: "function error 2"},
|
||||
})
|
||||
valueValidation := Failures[int](Errors{
|
||||
&ValidationError{Messsage: "value error 1"},
|
||||
&ValidationError{Messsage: "value error 2"},
|
||||
})
|
||||
|
||||
result := app.Ap(valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 4)
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "function error 1")
|
||||
assert.Contains(t, messages, "function error 2")
|
||||
assert.Contains(t, messages, "value error 1")
|
||||
assert.Contains(t, messages, "value error 2")
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeComposition(t *testing.T) {
|
||||
app := Applicative[int, int]()
|
||||
|
||||
t.Run("composes Map and Of", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
result := F.Pipe1(
|
||||
app.Of(21),
|
||||
app.Map(double),
|
||||
)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("composes multiple Map operations", func(t *testing.T) {
|
||||
app := Applicative[int, string]()
|
||||
double := func(x int) int { return x * 2 }
|
||||
toString := func(x int) string { return fmt.Sprintf("%d", x) }
|
||||
|
||||
result := F.Pipe2(
|
||||
app.Of(21),
|
||||
Map(double),
|
||||
app.Map(toString),
|
||||
)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "42", value)
|
||||
})
|
||||
|
||||
t.Run("composes Map and Ap", func(t *testing.T) {
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
|
||||
ioFunc := F.Pipe1(
|
||||
app.Of(5),
|
||||
Map(add),
|
||||
)
|
||||
valueValidation := app.Of(16)
|
||||
|
||||
result := app.Ap(valueValidation)(ioFunc)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 21, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeLawsWithInstance(t *testing.T) {
|
||||
app := Applicative[int, int]()
|
||||
|
||||
t.Run("identity law: Ap(Of(id))(v) == v", func(t *testing.T) {
|
||||
identity := func(x int) int { return x }
|
||||
v := app.Of(42)
|
||||
|
||||
left := app.Ap(v)(Of(identity))
|
||||
right := v
|
||||
|
||||
assert.Equal(t, right, left)
|
||||
})
|
||||
|
||||
t.Run("homomorphism law: Ap(Of(x))(Of(f)) == Of(f(x))", func(t *testing.T) {
|
||||
f := func(x int) int { return x * 2 }
|
||||
x := 21
|
||||
|
||||
left := app.Ap(app.Of(x))(Of(f))
|
||||
right := app.Of(f(x))
|
||||
|
||||
assert.Equal(t, right, left)
|
||||
})
|
||||
|
||||
t.Run("interchange law: Ap(Of(y))(u) == Ap(u)(Of($ y))", func(t *testing.T) {
|
||||
double := func(x int) int { return x * 2 }
|
||||
u := Of(double)
|
||||
y := 21
|
||||
|
||||
left := app.Ap(app.Of(y))(u)
|
||||
|
||||
applyY := func(f func(int) int) int { return f(y) }
|
||||
right := Ap[int](u)(Of(applyY))
|
||||
|
||||
assert.Equal(t, right, left)
|
||||
})
|
||||
|
||||
t.Run("identity law with failure", func(t *testing.T) {
|
||||
identity := func(x int) int { return x }
|
||||
v := Failures[int](Errors{&ValidationError{Messsage: "error"}})
|
||||
|
||||
left := app.Ap(v)(Of(identity))
|
||||
right := v
|
||||
|
||||
assert.Equal(t, right, left)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeMultipleArguments(t *testing.T) {
|
||||
app := Applicative[int, int]()
|
||||
|
||||
t.Run("applies curried two-argument function", func(t *testing.T) {
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
|
||||
// Create validation with curried function
|
||||
funcValidation := F.Pipe1(
|
||||
app.Of(10),
|
||||
Map(add),
|
||||
)
|
||||
|
||||
// Apply to second argument
|
||||
result := app.Ap(app.Of(32))(funcValidation)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("applies curried three-argument function", func(t *testing.T) {
|
||||
add3 := func(a int) func(int) func(int) int {
|
||||
return func(b int) func(int) int {
|
||||
return func(c int) int {
|
||||
return a + b + c
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build up the computation step by step
|
||||
funcValidation1 := F.Pipe1(
|
||||
app.Of(10),
|
||||
Map(add3),
|
||||
)
|
||||
|
||||
funcValidation2 := Ap[func(int) int](app.Of(20))(funcValidation1)
|
||||
result := Ap[int](app.Of(12))(funcValidation2)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("accumulates errors from multiple arguments", func(t *testing.T) {
|
||||
add := func(a int) func(int) int {
|
||||
return func(b int) int { return a + b }
|
||||
}
|
||||
|
||||
// First argument fails
|
||||
arg1 := Failures[int](Errors{&ValidationError{Messsage: "arg1 error"}})
|
||||
// Second argument fails
|
||||
arg2 := Failures[int](Errors{&ValidationError{Messsage: "arg2 error"}})
|
||||
|
||||
funcValidation := F.Pipe1(arg1, Map(add))
|
||||
result := app.Ap(arg2)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2)
|
||||
messages := []string{errors[0].Messsage, errors[1].Messsage}
|
||||
assert.Contains(t, messages, "arg1 error")
|
||||
assert.Contains(t, messages, "arg2 error")
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeWithDifferentTypes(t *testing.T) {
|
||||
t.Run("int to string", func(t *testing.T) {
|
||||
app := Applicative[int, string]()
|
||||
toString := func(x int) string { return fmt.Sprintf("%d", x) }
|
||||
result := app.Map(toString)(app.Of(42))
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "42", value)
|
||||
})
|
||||
|
||||
t.Run("string to int", func(t *testing.T) {
|
||||
app := Applicative[string, int]()
|
||||
toLength := func(s string) int { return len(s) }
|
||||
result := app.Map(toLength)(app.Of("hello"))
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 5, value)
|
||||
})
|
||||
|
||||
t.Run("bool to string", func(t *testing.T) {
|
||||
app := Applicative[bool, string]()
|
||||
toString := func(b bool) string {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
result := app.Map(toString)(app.Of(true))
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "true", value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeRealWorldScenario(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
t.Run("validates user with all valid fields", func(t *testing.T) {
|
||||
validateName := func(name string) Validation[string] {
|
||||
if len(name) < 3 {
|
||||
return Failures[string](Errors{&ValidationError{Messsage: "Name must be at least 3 characters"}})
|
||||
}
|
||||
return Success(name)
|
||||
}
|
||||
|
||||
validateAge := func(age int) Validation[int] {
|
||||
if age < 18 {
|
||||
return Failures[int](Errors{&ValidationError{Messsage: "Must be 18 or older"}})
|
||||
}
|
||||
return Success(age)
|
||||
}
|
||||
|
||||
validateEmail := func(email string) Validation[string] {
|
||||
if len(email) == 0 {
|
||||
return Failures[string](Errors{&ValidationError{Messsage: "Email is required"}})
|
||||
}
|
||||
return Success(email)
|
||||
}
|
||||
|
||||
makeUser := func(name string) func(int) func(string) User {
|
||||
return func(age int) func(string) User {
|
||||
return func(email string) User {
|
||||
return User{Name: name, Age: age, Email: email}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
name := validateName("Alice")
|
||||
age := validateAge(25)
|
||||
email := validateEmail("alice@example.com")
|
||||
|
||||
// Use the standalone Ap function with proper type parameters
|
||||
result := Ap[User](email)(Ap[func(string) User](age)(Ap[func(int) func(string) User](name)(Of(makeUser))))
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
user := either.MonadFold(result,
|
||||
func(Errors) User { return User{} },
|
||||
F.Identity[User],
|
||||
)
|
||||
assert.Equal(t, "Alice", user.Name)
|
||||
assert.Equal(t, 25, user.Age)
|
||||
assert.Equal(t, "alice@example.com", user.Email)
|
||||
})
|
||||
|
||||
t.Run("accumulates all validation errors", func(t *testing.T) {
|
||||
validateName := func(name string) Validation[string] {
|
||||
if len(name) < 3 {
|
||||
return Failures[string](Errors{&ValidationError{Messsage: "Name must be at least 3 characters"}})
|
||||
}
|
||||
return Success(name)
|
||||
}
|
||||
|
||||
validateAge := func(age int) Validation[int] {
|
||||
if age < 18 {
|
||||
return Failures[int](Errors{&ValidationError{Messsage: "Must be 18 or older"}})
|
||||
}
|
||||
return Success(age)
|
||||
}
|
||||
|
||||
validateEmail := func(email string) Validation[string] {
|
||||
if len(email) == 0 {
|
||||
return Failures[string](Errors{&ValidationError{Messsage: "Email is required"}})
|
||||
}
|
||||
return Success(email)
|
||||
}
|
||||
|
||||
makeUser := func(name string) func(int) func(string) User {
|
||||
return func(age int) func(string) User {
|
||||
return func(email string) User {
|
||||
return User{Name: name, Age: age, Email: email}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All validations fail
|
||||
name := validateName("ab")
|
||||
age := validateAge(16)
|
||||
email := validateEmail("")
|
||||
|
||||
// Use the standalone Ap function with proper type parameters
|
||||
result := Ap[User](email)(Ap[func(string) User](age)(Ap[func(int) func(string) User](name)(Of(makeUser))))
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(User) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 3)
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "Name must be at least 3 characters")
|
||||
assert.Contains(t, messages, "Must be 18 or older")
|
||||
assert.Contains(t, messages, "Email is required")
|
||||
})
|
||||
}
|
||||
54
v2/optics/codec/validation/monoid.go
Normal file
54
v2/optics/codec/validation/monoid.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
)
|
||||
|
||||
// ErrorsMonoid returns a Monoid instance for Errors (array of ValidationError pointers).
|
||||
// The monoid concatenates error arrays, with an empty array as the identity element.
|
||||
// This is used internally by the applicative operations to accumulate validation errors.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// m := ErrorsMonoid()
|
||||
// combined := m.Concat(errors1, errors2) // Concatenates both error arrays
|
||||
// empty := m.Empty() // Returns empty error array
|
||||
func ErrorsMonoid() Monoid[Errors] {
|
||||
return A.Monoid[*ValidationError]()
|
||||
}
|
||||
|
||||
// ApplicativeMonoid creates a Monoid instance for Validation[A] given a Monoid for A.
|
||||
// This allows combining validation results where the success values are also combined
|
||||
// using the provided monoid. If any validation fails, all errors are accumulated.
|
||||
//
|
||||
// The resulting monoid:
|
||||
// - Empty: Returns a successful validation with the empty value from the inner monoid
|
||||
// - Concat: Combines two validations:
|
||||
// - Both success: Combines values using the inner monoid
|
||||
// - Any failure: Accumulates all errors
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import "github.com/IBM/fp-go/v2/string"
|
||||
//
|
||||
// // Create a monoid for validations of strings
|
||||
// m := ApplicativeMonoid(string.Monoid)
|
||||
//
|
||||
// v1 := Success("Hello")
|
||||
// v2 := Success(" World")
|
||||
// combined := m.Concat(v1, v2) // Success("Hello World")
|
||||
//
|
||||
// v3 := Failures[string](someErrors)
|
||||
// failed := m.Concat(v1, v3) // Failures with accumulated errors
|
||||
func ApplicativeMonoid[A any](m Monoid[A]) Monoid[Validation[A]] {
|
||||
|
||||
return M.ApplicativeMonoid(
|
||||
Of,
|
||||
either.MonadMap,
|
||||
either.MonadApV[A, A](ErrorsMonoid()),
|
||||
|
||||
m,
|
||||
)
|
||||
}
|
||||
353
v2/optics/codec/validation/monoid_test.go
Normal file
353
v2/optics/codec/validation/monoid_test.go
Normal file
@@ -0,0 +1,353 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
MO "github.com/IBM/fp-go/v2/monoid"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestErrorsMonoid(t *testing.T) {
|
||||
m := ErrorsMonoid()
|
||||
|
||||
t.Run("empty returns empty array", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
assert.NotNil(t, empty)
|
||||
assert.Len(t, empty, 0)
|
||||
})
|
||||
|
||||
t.Run("concat combines error arrays", func(t *testing.T) {
|
||||
errs1 := Errors{
|
||||
&ValidationError{Messsage: "error 1"},
|
||||
&ValidationError{Messsage: "error 2"},
|
||||
}
|
||||
errs2 := Errors{
|
||||
&ValidationError{Messsage: "error 3"},
|
||||
}
|
||||
|
||||
result := m.Concat(errs1, errs2)
|
||||
|
||||
assert.Len(t, result, 3)
|
||||
assert.Equal(t, "error 1", result[0].Messsage)
|
||||
assert.Equal(t, "error 2", result[1].Messsage)
|
||||
assert.Equal(t, "error 3", result[2].Messsage)
|
||||
})
|
||||
|
||||
t.Run("concat with empty preserves errors", func(t *testing.T) {
|
||||
errs := Errors{
|
||||
&ValidationError{Messsage: "error"},
|
||||
}
|
||||
empty := m.Empty()
|
||||
|
||||
result1 := m.Concat(errs, empty)
|
||||
result2 := m.Concat(empty, errs)
|
||||
|
||||
assert.Equal(t, errs, result1)
|
||||
assert.Equal(t, errs, result2)
|
||||
})
|
||||
|
||||
t.Run("concat is associative", func(t *testing.T) {
|
||||
errs1 := Errors{&ValidationError{Messsage: "1"}}
|
||||
errs2 := Errors{&ValidationError{Messsage: "2"}}
|
||||
errs3 := Errors{&ValidationError{Messsage: "3"}}
|
||||
|
||||
// (a + b) + c
|
||||
left := m.Concat(m.Concat(errs1, errs2), errs3)
|
||||
// a + (b + c)
|
||||
right := m.Concat(errs1, m.Concat(errs2, errs3))
|
||||
|
||||
assert.Len(t, left, 3)
|
||||
assert.Len(t, right, 3)
|
||||
for i := 0; i < 3; i++ {
|
||||
assert.Equal(t, left[i].Messsage, right[i].Messsage)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeMonoid(t *testing.T) {
|
||||
t.Run("with string monoid", func(t *testing.T) {
|
||||
m := ApplicativeMonoid(S.Monoid)
|
||||
|
||||
t.Run("empty returns successful validation with empty string", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
|
||||
assert.True(t, either.IsRight(empty))
|
||||
value := either.MonadFold(empty,
|
||||
func(Errors) string { return "ERROR" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "", value)
|
||||
})
|
||||
|
||||
t.Run("concat combines successful validations", func(t *testing.T) {
|
||||
v1 := Success("Hello")
|
||||
v2 := Success(" World")
|
||||
|
||||
result := m.Concat(v1, v2)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "Hello World", value)
|
||||
})
|
||||
|
||||
t.Run("concat with failure returns failure", func(t *testing.T) {
|
||||
v1 := Success("Hello")
|
||||
v2 := Failures[string](Errors{
|
||||
&ValidationError{Messsage: "error"},
|
||||
})
|
||||
|
||||
result := m.Concat(v1, v2)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("concat accumulates all errors from both failures", func(t *testing.T) {
|
||||
v1 := Failures[string](Errors{
|
||||
&ValidationError{Messsage: "error 1"},
|
||||
})
|
||||
v2 := Failures[string](Errors{
|
||||
&ValidationError{Messsage: "error 2"},
|
||||
})
|
||||
|
||||
result := m.Concat(v1, v2)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2)
|
||||
messages := []string{errors[0].Messsage, errors[1].Messsage}
|
||||
assert.Contains(t, messages, "error 1")
|
||||
assert.Contains(t, messages, "error 2")
|
||||
})
|
||||
|
||||
t.Run("concat with empty preserves validation", func(t *testing.T) {
|
||||
v := Success("test")
|
||||
empty := m.Empty()
|
||||
|
||||
result1 := m.Concat(v, empty)
|
||||
result2 := m.Concat(empty, v)
|
||||
|
||||
val1 := either.MonadFold(result1,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
val2 := either.MonadFold(result2,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
assert.Equal(t, "test", val1)
|
||||
assert.Equal(t, "test", val2)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with int addition monoid", func(t *testing.T) {
|
||||
intMonoid := MO.MakeMonoid(
|
||||
func(a, b int) int { return a + b },
|
||||
0,
|
||||
)
|
||||
|
||||
m := ApplicativeMonoid(intMonoid)
|
||||
|
||||
t.Run("empty returns zero", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
|
||||
value := either.MonadFold(empty,
|
||||
func(Errors) int { return -1 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 0, value)
|
||||
})
|
||||
|
||||
t.Run("concat adds values", func(t *testing.T) {
|
||||
v1 := Success(10)
|
||||
v2 := Success(32)
|
||||
|
||||
result := m.Concat(v1, v2)
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("multiple concat operations", func(t *testing.T) {
|
||||
v1 := Success(1)
|
||||
v2 := Success(2)
|
||||
v3 := Success(3)
|
||||
v4 := Success(4)
|
||||
|
||||
result := m.Concat(m.Concat(m.Concat(v1, v2), v3), v4)
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 10, value)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonoidLaws(t *testing.T) {
|
||||
t.Run("ErrorsMonoid satisfies monoid laws", func(t *testing.T) {
|
||||
m := ErrorsMonoid()
|
||||
|
||||
errs1 := Errors{&ValidationError{Messsage: "1"}}
|
||||
errs2 := Errors{&ValidationError{Messsage: "2"}}
|
||||
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
// empty + a = a
|
||||
result := m.Concat(m.Empty(), errs1)
|
||||
assert.Equal(t, errs1, result)
|
||||
})
|
||||
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
// a + empty = a
|
||||
result := m.Concat(errs1, m.Empty())
|
||||
assert.Equal(t, errs1, result)
|
||||
})
|
||||
|
||||
t.Run("associativity", func(t *testing.T) {
|
||||
errs3 := Errors{&ValidationError{Messsage: "3"}}
|
||||
// (a + b) + c = a + (b + c)
|
||||
left := m.Concat(m.Concat(errs1, errs2), errs3)
|
||||
right := m.Concat(errs1, m.Concat(errs2, errs3))
|
||||
|
||||
assert.Len(t, left, 3)
|
||||
assert.Len(t, right, 3)
|
||||
for i := 0; i < 3; i++ {
|
||||
assert.Equal(t, left[i].Messsage, right[i].Messsage)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("ApplicativeMonoid satisfies monoid laws", func(t *testing.T) {
|
||||
m := ApplicativeMonoid(S.Monoid)
|
||||
|
||||
v1 := Success("a")
|
||||
v2 := Success("b")
|
||||
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
// empty + a = a
|
||||
result := m.Concat(m.Empty(), v1)
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "a", value)
|
||||
})
|
||||
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
// a + empty = a
|
||||
result := m.Concat(v1, m.Empty())
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "a", value)
|
||||
})
|
||||
|
||||
t.Run("associativity", func(t *testing.T) {
|
||||
v3 := Success("c")
|
||||
// (a + b) + c = a + (b + c)
|
||||
left := m.Concat(m.Concat(v1, v2), v3)
|
||||
right := m.Concat(v1, m.Concat(v2, v3))
|
||||
|
||||
leftVal := either.MonadFold(left,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
rightVal := either.MonadFold(right,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
assert.Equal(t, "abc", leftVal)
|
||||
assert.Equal(t, "abc", rightVal)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeMonoidWithFailures(t *testing.T) {
|
||||
m := ApplicativeMonoid(S.Monoid)
|
||||
|
||||
t.Run("failure propagates through concat", func(t *testing.T) {
|
||||
v1 := Success("a")
|
||||
v2 := Failures[string](Errors{&ValidationError{Messsage: "error"}})
|
||||
v3 := Success("c")
|
||||
|
||||
result := m.Concat(m.Concat(v1, v2), v3)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
})
|
||||
|
||||
t.Run("multiple failures accumulate", func(t *testing.T) {
|
||||
v1 := Failures[string](Errors{&ValidationError{Messsage: "error 1"}})
|
||||
v2 := Failures[string](Errors{&ValidationError{Messsage: "error 2"}})
|
||||
v3 := Failures[string](Errors{&ValidationError{Messsage: "error 3"}})
|
||||
|
||||
result := m.Concat(m.Concat(v1, v2), v3)
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 3)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeMonoidEdgeCases(t *testing.T) {
|
||||
t.Run("with custom struct monoid", func(t *testing.T) {
|
||||
type Counter struct{ Count int }
|
||||
|
||||
counterMonoid := MO.MakeMonoid(
|
||||
func(a, b Counter) Counter { return Counter{Count: a.Count + b.Count} },
|
||||
Counter{Count: 0},
|
||||
)
|
||||
|
||||
m := ApplicativeMonoid(counterMonoid)
|
||||
|
||||
v1 := Success(Counter{Count: 5})
|
||||
v2 := Success(Counter{Count: 10})
|
||||
|
||||
result := m.Concat(v1, v2)
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) Counter { return Counter{} },
|
||||
F.Identity[Counter],
|
||||
)
|
||||
assert.Equal(t, 15, value.Count)
|
||||
})
|
||||
|
||||
t.Run("empty concat empty", func(t *testing.T) {
|
||||
m := ApplicativeMonoid(S.Monoid)
|
||||
|
||||
result := m.Concat(m.Empty(), m.Empty())
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "ERROR" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "", value)
|
||||
})
|
||||
}
|
||||
49
v2/optics/codec/validation/types.go
Normal file
49
v2/optics/codec/validation/types.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
// Either represents a value that can be one of two types: Left (error) or Right (success).
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
|
||||
// ContextEntry represents a single entry in the validation context path.
|
||||
// It tracks the location and type information during nested validation.
|
||||
ContextEntry struct {
|
||||
Key string // The key or field name (for objects/maps)
|
||||
Type string // The expected type name
|
||||
Actual any // The actual value being validated
|
||||
}
|
||||
|
||||
// Context is a stack of ContextEntry values representing the path through
|
||||
// nested structures during validation. Used to provide detailed error messages.
|
||||
Context = []ContextEntry
|
||||
|
||||
// ValidationError represents a single validation failure with context.
|
||||
ValidationError struct {
|
||||
Value any // The value that failed validation
|
||||
Context Context // The path to the value in nested structures
|
||||
Messsage string // Human-readable error message
|
||||
Cause error
|
||||
}
|
||||
|
||||
// Errors is a collection of validation errors.
|
||||
Errors = []*ValidationError
|
||||
|
||||
// Validation represents the result of a validation operation.
|
||||
// Left contains validation errors, Right contains the successfully validated value.
|
||||
Validation[A any] = Either[Errors, A]
|
||||
|
||||
// Reader represents a computation that depends on an environment R and produces a value A.
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
|
||||
Kleisli[A, B any] = Reader[A, Validation[B]]
|
||||
|
||||
Operator[A, B any] = Kleisli[Validation[A], B]
|
||||
|
||||
Monoid[A any] = monoid.Monoid[A]
|
||||
)
|
||||
125
v2/optics/codec/validation/validation.go
Normal file
125
v2/optics/codec/validation/validation.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
)
|
||||
|
||||
// Error implements the error interface for ValidationError.
|
||||
// Returns a generic error message indicating this is a validation error.
|
||||
// For detailed error information, use String() or Format() methods.
|
||||
|
||||
// Error implements the error interface for ValidationError.
|
||||
// Returns a generic error message.
|
||||
func (v *ValidationError) Error() string {
|
||||
return "ValidationError"
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying cause error if present.
|
||||
// This allows ValidationError to work with errors.Is and errors.As.
|
||||
func (v *ValidationError) Unwrap() error {
|
||||
return v.Cause
|
||||
}
|
||||
|
||||
// String returns a simple string representation of the validation error.
|
||||
// Returns the error message prefixed with "ValidationError: ".
|
||||
func (v *ValidationError) String() string {
|
||||
return fmt.Sprintf("ValidationError: %s", v.Messsage)
|
||||
}
|
||||
|
||||
// Format implements fmt.Formatter for custom formatting of ValidationError.
|
||||
// It includes the context path, message, and optionally the cause error.
|
||||
// Supports verbs: %s, %v, %+v (with additional details)
|
||||
func (v *ValidationError) Format(s fmt.State, verb rune) {
|
||||
// Build the context path
|
||||
path := ""
|
||||
for i, entry := range v.Context {
|
||||
if i > 0 {
|
||||
path += "."
|
||||
}
|
||||
if entry.Key != "" {
|
||||
path += entry.Key
|
||||
} else {
|
||||
path += entry.Type
|
||||
}
|
||||
}
|
||||
|
||||
// Start with the path if available
|
||||
result := ""
|
||||
if path != "" {
|
||||
result = fmt.Sprintf("at %s: ", path)
|
||||
}
|
||||
|
||||
// Add the message
|
||||
result += v.Messsage
|
||||
|
||||
// Add the cause if present
|
||||
if v.Cause != nil {
|
||||
if s.Flag('+') && verb == 'v' {
|
||||
// Verbose format with detailed cause
|
||||
result += fmt.Sprintf("\n caused by: %+v", v.Cause)
|
||||
} else {
|
||||
result += fmt.Sprintf(" (caused by: %v)", v.Cause)
|
||||
}
|
||||
}
|
||||
|
||||
// Add value information for verbose format
|
||||
if s.Flag('+') && verb == 'v' {
|
||||
result += fmt.Sprintf("\n value: %#v", v.Value)
|
||||
}
|
||||
|
||||
fmt.Fprint(s, result)
|
||||
}
|
||||
|
||||
// Failures creates a validation failure from a collection of errors.
|
||||
// Returns a Left Either containing the errors.
|
||||
func Failures[T any](err Errors) Validation[T] {
|
||||
return either.Left[T](err)
|
||||
}
|
||||
|
||||
// FailureWithMessage creates a validation failure with a custom message.
|
||||
// Returns a Reader that takes a Context and produces a Validation[T] failure.
|
||||
// This is useful for creating context-aware validation errors.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// fail := FailureWithMessage[int]("abc", "expected integer")
|
||||
// result := fail([]ContextEntry{{Key: "age", Type: "int"}})
|
||||
func FailureWithMessage[T any](value any, message string) Reader[Context, Validation[T]] {
|
||||
return func(context Context) Validation[T] {
|
||||
return Failures[T](A.Of(&ValidationError{
|
||||
Value: value,
|
||||
Context: context,
|
||||
Messsage: message,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// FailureWithError creates a validation failure with a custom message and underlying cause.
|
||||
// Returns a Reader that takes an error, then a Context, and produces a Validation[T] failure.
|
||||
// This is useful for wrapping errors from other operations while maintaining validation context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// fail := FailureWithError[int]("abc", "parse failed")
|
||||
// result := fail(parseErr)([]ContextEntry{{Key: "count", Type: "int"}})
|
||||
func FailureWithError[T any](value any, message string) Reader[error, Reader[Context, Validation[T]]] {
|
||||
return func(err error) Reader[Context, Validation[T]] {
|
||||
return func(context Context) Validation[T] {
|
||||
return Failures[T](A.Of(&ValidationError{
|
||||
Value: value,
|
||||
Context: context,
|
||||
Messsage: message,
|
||||
Cause: err,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Success creates a successful validation result.
|
||||
// Returns a Right Either containing the validated value.
|
||||
func Success[T any](value T) Validation[T] {
|
||||
return either.Of[Errors](value)
|
||||
}
|
||||
419
v2/optics/codec/validation/validation_test.go
Normal file
419
v2/optics/codec/validation/validation_test.go
Normal file
@@ -0,0 +1,419 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidationError_Error(t *testing.T) {
|
||||
err := &ValidationError{
|
||||
Value: "test",
|
||||
Messsage: "invalid value",
|
||||
}
|
||||
|
||||
assert.Equal(t, "ValidationError", err.Error())
|
||||
}
|
||||
|
||||
func TestValidationError_String(t *testing.T) {
|
||||
err := &ValidationError{
|
||||
Value: "test",
|
||||
Messsage: "invalid value",
|
||||
}
|
||||
|
||||
expected := "ValidationError: invalid value"
|
||||
assert.Equal(t, expected, err.String())
|
||||
}
|
||||
|
||||
func TestValidationError_Unwrap(t *testing.T) {
|
||||
t.Run("with cause", func(t *testing.T) {
|
||||
cause := errors.New("underlying error")
|
||||
err := &ValidationError{
|
||||
Value: "test",
|
||||
Messsage: "invalid value",
|
||||
Cause: cause,
|
||||
}
|
||||
|
||||
assert.Equal(t, cause, err.Unwrap())
|
||||
})
|
||||
|
||||
t.Run("without cause", func(t *testing.T) {
|
||||
err := &ValidationError{
|
||||
Value: "test",
|
||||
Messsage: "invalid value",
|
||||
}
|
||||
|
||||
assert.Nil(t, err.Unwrap())
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidationError_Format(t *testing.T) {
|
||||
t.Run("simple format without context", func(t *testing.T) {
|
||||
err := &ValidationError{
|
||||
Value: "test",
|
||||
Messsage: "invalid value",
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("%v", err)
|
||||
assert.Equal(t, "invalid value", result)
|
||||
})
|
||||
|
||||
t.Run("with context path", func(t *testing.T) {
|
||||
err := &ValidationError{
|
||||
Value: "test",
|
||||
Context: []ContextEntry{{Key: "user"}, {Key: "name"}},
|
||||
Messsage: "must not be empty",
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("%v", err)
|
||||
assert.Equal(t, "at user.name: must not be empty", result)
|
||||
})
|
||||
|
||||
t.Run("with context using type", func(t *testing.T) {
|
||||
err := &ValidationError{
|
||||
Value: 123,
|
||||
Context: []ContextEntry{{Type: "User"}, {Key: "age"}},
|
||||
Messsage: "must be positive",
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("%v", err)
|
||||
assert.Equal(t, "at User.age: must be positive", result)
|
||||
})
|
||||
|
||||
t.Run("with cause - simple format", func(t *testing.T) {
|
||||
cause := errors.New("parse error")
|
||||
err := &ValidationError{
|
||||
Value: "abc",
|
||||
Messsage: "invalid number",
|
||||
Cause: cause,
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("%v", err)
|
||||
assert.Equal(t, "invalid number (caused by: parse error)", result)
|
||||
})
|
||||
|
||||
t.Run("with cause - verbose format", func(t *testing.T) {
|
||||
cause := errors.New("parse error")
|
||||
err := &ValidationError{
|
||||
Value: "abc",
|
||||
Messsage: "invalid number",
|
||||
Cause: cause,
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("%+v", err)
|
||||
assert.Contains(t, result, "invalid number")
|
||||
assert.Contains(t, result, "caused by: parse error")
|
||||
assert.Contains(t, result, `value: "abc"`)
|
||||
})
|
||||
|
||||
t.Run("verbose format shows value", func(t *testing.T) {
|
||||
err := &ValidationError{
|
||||
Value: 42,
|
||||
Messsage: "out of range",
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("%+v", err)
|
||||
assert.Contains(t, result, "out of range")
|
||||
assert.Contains(t, result, "value: 42")
|
||||
})
|
||||
|
||||
t.Run("complex context path", func(t *testing.T) {
|
||||
err := &ValidationError{
|
||||
Value: "invalid",
|
||||
Context: []ContextEntry{
|
||||
{Key: "user"},
|
||||
{Key: "address"},
|
||||
{Key: "zipCode"},
|
||||
},
|
||||
Messsage: "invalid format",
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("%v", err)
|
||||
assert.Equal(t, "at user.address.zipCode: invalid format", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFailures(t *testing.T) {
|
||||
t.Run("creates left either with errors", func(t *testing.T) {
|
||||
errs := Errors{
|
||||
&ValidationError{Value: "test", Messsage: "error 1"},
|
||||
&ValidationError{Value: "test", Messsage: "error 2"},
|
||||
}
|
||||
|
||||
result := Failures[int](errs)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
left := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, left, 2)
|
||||
assert.Equal(t, "error 1", left[0].Messsage)
|
||||
assert.Equal(t, "error 2", left[1].Messsage)
|
||||
})
|
||||
|
||||
t.Run("preserves error details", func(t *testing.T) {
|
||||
errs := Errors{
|
||||
&ValidationError{
|
||||
Value: "abc",
|
||||
Context: []ContextEntry{{Key: "field"}},
|
||||
Messsage: "invalid",
|
||||
Cause: errors.New("cause"),
|
||||
},
|
||||
}
|
||||
|
||||
result := Failures[string](errs)
|
||||
|
||||
left := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
require.Len(t, left, 1)
|
||||
assert.Equal(t, "abc", left[0].Value)
|
||||
assert.Equal(t, "invalid", left[0].Messsage)
|
||||
assert.NotNil(t, left[0].Cause)
|
||||
assert.Len(t, left[0].Context, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSuccess(t *testing.T) {
|
||||
t.Run("creates right either with value", func(t *testing.T) {
|
||||
result := Success(42)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
strResult := Success("hello")
|
||||
str := either.MonadFold(strResult,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "hello", str)
|
||||
|
||||
boolResult := Success(true)
|
||||
b := either.MonadFold(boolResult,
|
||||
func(Errors) bool { return false },
|
||||
F.Identity[bool],
|
||||
)
|
||||
assert.Equal(t, true, b)
|
||||
|
||||
type Custom struct{ Name string }
|
||||
customResult := Success(Custom{Name: "test"})
|
||||
custom := either.MonadFold(customResult,
|
||||
func(Errors) Custom { return Custom{} },
|
||||
F.Identity[Custom],
|
||||
)
|
||||
assert.Equal(t, "test", custom.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFailureWithMessage(t *testing.T) {
|
||||
t.Run("creates failure with context", func(t *testing.T) {
|
||||
fail := FailureWithMessage[int]("abc", "expected integer")
|
||||
context := []ContextEntry{{Key: "age", Type: "int"}}
|
||||
|
||||
result := fail(context)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errs := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
require.Len(t, errs, 1)
|
||||
assert.Equal(t, "abc", errs[0].Value)
|
||||
assert.Equal(t, "expected integer", errs[0].Messsage)
|
||||
assert.Equal(t, context, errs[0].Context)
|
||||
assert.Nil(t, errs[0].Cause)
|
||||
})
|
||||
|
||||
t.Run("works with empty context", func(t *testing.T) {
|
||||
fail := FailureWithMessage[string](123, "wrong type")
|
||||
result := fail(nil)
|
||||
|
||||
errs := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
require.Len(t, errs, 1)
|
||||
assert.Equal(t, 123, errs[0].Value)
|
||||
assert.Nil(t, errs[0].Context)
|
||||
})
|
||||
|
||||
t.Run("preserves complex context", func(t *testing.T) {
|
||||
fail := FailureWithMessage[bool]("not a bool", "type mismatch")
|
||||
context := []ContextEntry{
|
||||
{Key: "user"},
|
||||
{Key: "settings"},
|
||||
{Key: "enabled"},
|
||||
}
|
||||
|
||||
result := fail(context)
|
||||
|
||||
errs := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(bool) Errors { return nil },
|
||||
)
|
||||
require.Len(t, errs, 1)
|
||||
assert.Equal(t, context, errs[0].Context)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFailureWithError(t *testing.T) {
|
||||
t.Run("creates failure with cause and context", func(t *testing.T) {
|
||||
cause := errors.New("parse failed")
|
||||
fail := FailureWithError[int]("abc", "invalid number")
|
||||
context := []ContextEntry{{Key: "count"}}
|
||||
|
||||
result := fail(cause)(context)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errs := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
require.Len(t, errs, 1)
|
||||
assert.Equal(t, "abc", errs[0].Value)
|
||||
assert.Equal(t, "invalid number", errs[0].Messsage)
|
||||
assert.Equal(t, context, errs[0].Context)
|
||||
assert.Equal(t, cause, errs[0].Cause)
|
||||
})
|
||||
|
||||
t.Run("cause is unwrappable", func(t *testing.T) {
|
||||
cause := errors.New("underlying")
|
||||
fail := FailureWithError[string](nil, "wrapper")
|
||||
result := fail(cause)(nil)
|
||||
|
||||
errs := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
require.Len(t, errs, 1)
|
||||
assert.True(t, errors.Is(errs[0], cause))
|
||||
})
|
||||
|
||||
t.Run("works with complex error chain", func(t *testing.T) {
|
||||
root := errors.New("root cause")
|
||||
wrapped := fmt.Errorf("wrapped: %w", root)
|
||||
fail := FailureWithError[int](0, "validation failed")
|
||||
|
||||
result := fail(wrapped)([]ContextEntry{{Key: "field"}})
|
||||
|
||||
errs := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
require.Len(t, errs, 1)
|
||||
assert.True(t, errors.Is(errs[0], root))
|
||||
assert.True(t, errors.Is(errs[0], wrapped))
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidationIntegration(t *testing.T) {
|
||||
t.Run("success and failure can be combined", func(t *testing.T) {
|
||||
success := Success(42)
|
||||
failure := Failures[int](Errors{
|
||||
&ValidationError{Value: "bad", Messsage: "error"},
|
||||
})
|
||||
|
||||
assert.True(t, either.IsRight(success))
|
||||
assert.True(t, either.IsLeft(failure))
|
||||
})
|
||||
|
||||
t.Run("context provides meaningful error paths", func(t *testing.T) {
|
||||
fail := FailureWithMessage[string](nil, "required field")
|
||||
context := []ContextEntry{
|
||||
{Key: "request"},
|
||||
{Key: "body"},
|
||||
{Key: "user"},
|
||||
{Key: "email"},
|
||||
}
|
||||
|
||||
result := fail(context)
|
||||
errs := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
|
||||
formatted := fmt.Sprintf("%v", errs[0])
|
||||
assert.Contains(t, formatted, "request.body.user.email")
|
||||
assert.Contains(t, formatted, "required field")
|
||||
})
|
||||
|
||||
t.Run("multiple errors can be collected", func(t *testing.T) {
|
||||
errs := Errors{
|
||||
&ValidationError{
|
||||
Context: []ContextEntry{{Key: "name"}},
|
||||
Messsage: "too short",
|
||||
},
|
||||
&ValidationError{
|
||||
Context: []ContextEntry{{Key: "age"}},
|
||||
Messsage: "must be positive",
|
||||
},
|
||||
&ValidationError{
|
||||
Context: []ContextEntry{{Key: "email"}},
|
||||
Messsage: "invalid format",
|
||||
},
|
||||
}
|
||||
|
||||
result := Failures[any](errs)
|
||||
collected := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(any) Errors { return nil },
|
||||
)
|
||||
|
||||
assert.Len(t, collected, 3)
|
||||
messages := make([]string, len(collected))
|
||||
for i, err := range collected {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "too short")
|
||||
assert.Contains(t, messages, "must be positive")
|
||||
assert.Contains(t, messages, "invalid format")
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidationError_FormatEdgeCases(t *testing.T) {
|
||||
t.Run("empty message", func(t *testing.T) {
|
||||
err := &ValidationError{
|
||||
Value: "test",
|
||||
Messsage: "",
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("%v", err)
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
|
||||
t.Run("context with empty keys", func(t *testing.T) {
|
||||
err := &ValidationError{
|
||||
Value: "test",
|
||||
Context: []ContextEntry{{Key: ""}, {Type: "Type"}, {Key: ""}},
|
||||
Messsage: "error",
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("%v", err)
|
||||
// Should handle empty keys gracefully
|
||||
assert.Contains(t, result, "error")
|
||||
})
|
||||
|
||||
t.Run("nil value", func(t *testing.T) {
|
||||
err := &ValidationError{
|
||||
Value: nil,
|
||||
Messsage: "nil not allowed",
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("%+v", err)
|
||||
assert.Contains(t, result, "nil not allowed")
|
||||
assert.Contains(t, result, "value: <nil>")
|
||||
})
|
||||
}
|
||||
11
v2/optics/decoder/types.go
Normal file
11
v2/optics/decoder/types.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package decoder
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
type (
|
||||
Result[A any] = result.Result[A]
|
||||
|
||||
Decoder[I, A any] = result.Kleisli[I, A]
|
||||
)
|
||||
7
v2/optics/encoder/types.go
Normal file
7
v2/optics/encoder/types.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package encoder
|
||||
|
||||
import "github.com/IBM/fp-go/v2/reader"
|
||||
|
||||
type (
|
||||
Encoder[O, A any] = reader.Reader[A, O]
|
||||
)
|
||||
@@ -59,8 +59,5 @@ func Compose[
|
||||
return G.Compose[
|
||||
G.Traversal[A, B, HKTA, HKTB],
|
||||
G.Traversal[S, A, HKTS, HKTA],
|
||||
G.Traversal[S, B, HKTS, HKTB],
|
||||
S, A, B,
|
||||
HKTS, HKTA, HKTB,
|
||||
](ab)
|
||||
G.Traversal[S, B, HKTS, HKTB]](ab)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user