readerresult

package
v2.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 18, 2025 License: Apache-2.0 Imports: 22 Imported by: 0

README

🎯 ReaderResult: Context-Aware Functional Composition

📖 Overview

The ReaderResult monad is a specialized implementation of the Reader monad pattern for Go, designed specifically for functions that:

  • Depend on context.Context (for cancellation, deadlines, or context values)
  • May fail with an error
  • Need to be composed in a functional, declarative style
type ReaderResult[A any] func(context.Context) (A, error)

This is equivalent to the common Go pattern func(ctx context.Context) (A, error), but wrapped in a way that enables powerful functional composition.

🔄 ReaderResult as an Effectful Operation

Important: ReaderResult represents an effectful operation because it depends on context.Context, which is inherently mutable and can change during execution.

Why Context Makes ReaderResult Effectful

The context.Context type in Go is designed to be mutable in the following ways:

  1. Cancellation State: A context can transition from active to cancelled at any time
  2. Deadline Changes: Timeouts and deadlines can expire during execution
  3. Value Storage: Context values can be added or modified through context.WithValue
  4. Parent-Child Relationships: Derived contexts inherit and can override parent behavior

This mutability means that:

  • Same ReaderResult, Different Results: Executing the same ReaderResult with different contexts (or the same context at different times) can produce different outcomes
  • Non-Deterministic Behavior: Context cancellation or timeout can interrupt execution at any point
  • Side Effects: The context carries runtime state that affects computation behavior
Use Case Examples
1. HTTP Request Handling with Timeout
// The context carries a deadline that can expire during execution
func FetchUserProfile(userID int) ReaderResult[UserProfile] {
    return func(ctx context.Context) (UserProfile, error) {
        // Context deadline affects when this operation fails
        req, _ := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("/users/%d", userID), nil)
        resp, err := http.DefaultClient.Do(req)
        if err != nil {
            return UserProfile{}, err // May fail due to context timeout
        }
        defer resp.Body.Close()
        
        var profile UserProfile
        json.NewDecoder(resp.Body).Decode(&profile)
        return profile, nil
    }
}

// Same function, different contexts = different behavior
ctx1, cancel1 := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel1()
profile1, _ := FetchUserProfile(123)(ctx1) // Has 5 seconds to complete

ctx2, cancel2 := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel2()
profile2, _ := FetchUserProfile(123)(ctx2) // Has only 100ms - likely to timeout
2. Database Transactions with Cancellation
// Context cancellation can abort the transaction at any point
func TransferFunds(from, to int, amount float64) ReaderResult[Transaction] {
    return func(ctx context.Context) (Transaction, error) {
        tx, err := db.BeginTx(ctx, nil) // Context controls transaction lifetime
        if err != nil {
            return Transaction{}, err
        }
        defer tx.Rollback()
        
        // If context is cancelled here, the debit fails
        if err := debitAccount(ctx, tx, from, amount); err != nil {
            return Transaction{}, err
        }
        
        // Or cancellation could happen here, before credit
        if err := creditAccount(ctx, tx, to, amount); err != nil {
            return Transaction{}, err
        }
        
        if err := tx.Commit(); err != nil {
            return Transaction{}, err
        }
        
        return Transaction{From: from, To: to, Amount: amount}, nil
    }
}

// User cancels the request mid-transaction
ctx, cancel := context.WithCancel(context.Background())
go func() {
    time.Sleep(50 * time.Millisecond)
    cancel() // Cancellation affects the running operation
}()
result, err := TransferFunds(100, 200, 50.0)(ctx) // May be interrupted
3. Context Values for Request Tracing
// Context values affect logging and tracing behavior
func ProcessOrder(orderID string) ReaderResult[Order] {
    return func(ctx context.Context) (Order, error) {
        // Extract trace ID from context (mutable state)
        traceID := ctx.Value("trace-id")
        log.Printf("[%v] Processing order %s", traceID, orderID)
        
        // The same function behaves differently based on context values
        if ctx.Value("debug") == true {
            log.Printf("[%v] Debug mode: detailed order processing", traceID)
        }
        
        return fetchOrder(ctx, orderID)
    }
}

// Different contexts = different tracing behavior
ctx1 := context.WithValue(context.Background(), "trace-id", "req-001")
order1, _ := ProcessOrder("ORD-123")(ctx1) // Logs with trace-id: req-001

ctx2 := context.WithValue(context.Background(), "trace-id", "req-002")
ctx2 = context.WithValue(ctx2, "debug", true)
order2, _ := ProcessOrder("ORD-123")(ctx2) // Logs with trace-id: req-002 + debug info
4. Parallel Operations with Shared Cancellation
// Multiple operations share the same cancellable context
func FetchDashboardData(userID int) ReaderResult[Dashboard] {
    return func(ctx context.Context) (Dashboard, error) {
        // All these operations can be cancelled together
        userCh := make(chan User)
        postsCh := make(chan []Post)
        statsCh := make(chan Stats)
        errCh := make(chan error, 3)
        
        go func() {
            user, err := FetchUser(userID)(ctx) // Shares cancellation
            if err != nil {
                errCh <- err
                return
            }
            userCh <- user
        }()
        
        go func() {
            posts, err := FetchPosts(userID)(ctx) // Shares cancellation
            if err != nil {
                errCh <- err
                return
            }
            postsCh <- posts
        }()
        
        go func() {
            stats, err := FetchStats(userID)(ctx) // Shares cancellation
            if err != nil {
                errCh <- err
                return
            }
            statsCh <- stats
        }()
        
        // If context is cancelled, all goroutines stop
        select {
        case err := <-errCh:
            return Dashboard{}, err
        case <-ctx.Done():
            return Dashboard{}, ctx.Err() // Context cancellation is an effect
        case user := <-userCh:
            // ... collect results
        }
    }
}
5. Retry Logic with Context Awareness
import (
    "time"
    R "github.com/IBM/fp-go/v2/retry"
)

// Context state affects retry behavior using the built-in Retrying method
func FetchWithRetry[A any](operation ReaderResult[A]) ReaderResult[A] {
    // Create a retry policy: exponential backoff with a cap, limited to 5 retries
    policy := R.Monoid.Concat(
        R.LimitRetries(5),
        R.CapDelay(10*time.Second, R.ExponentialBackoff(100*time.Millisecond)),
    )
    
    // Check function: retry on any error
    // Note: context cancellation is automatically handled by Retrying
    shouldRetry := func(val A, err error) bool {
        return err != nil
    }
    
    // Use the built-in Retrying method with automatic context awareness
    // R.Always creates a constant function that ignores RetryStatus and always returns the operation
    return Retrying(policy, R.Always(operation), shouldRetry)
}

// Example usage:
// fetchUser := FetchWithRetry(GetUser(123))
// user, err := fetchUser(ctx) // Automatically retries with exponential backoff
//                              // and respects context cancellation
Key Takeaway

Because ReaderResult depends on the mutable context.Context, it represents effectful computations where:

  • Execution behavior can change based on context state
  • The same operation can produce different results with different contexts
  • External factors (timeouts, cancellations, context values) influence outcomes
  • Side effects are inherent due to context's runtime nature

This makes ReaderResult ideal for modeling real-world operations that interact with external systems, respect cancellation, and need to be composed in a functional style while acknowledging their effectful nature.

🤔 Why Use ReaderResult Instead of Traditional Go Methods?

1. ✨ Simplified API Design

Traditional Go approach:

func GetUser(ctx context.Context, id int) (User, error)
func GetPosts(ctx context.Context, userID int) ([]Post, error)
func FormatUser(ctx context.Context, user User) (string, error)

Every function must explicitly accept and thread context.Context through the call chain, leading to repetitive boilerplate.

ReaderResult approach:

func GetUser(id int) ReaderResult[User]
func GetPosts(userID int) ReaderResult[[]Post]
func FormatUser(user User) ReaderResult[string]

The context dependency is implicit in the return type. Functions are cleaner and focus on their core logic.

2. 🔗 Composability Through Monadic Operations

Traditional Go approach:

func GetUserWithPosts(ctx context.Context, userID int) (UserWithPosts, error) {
    user, err := GetUser(ctx, userID)
    if err != nil {
        return UserWithPosts{}, err
    }
    
    posts, err := GetPosts(ctx, user.ID)
    if err != nil {
        return UserWithPosts{}, err
    }
    
    formatted, err := FormatUser(ctx, user)
    if err != nil {
        return UserWithPosts{}, err
    }
    
    return UserWithPosts{
        User:      user,
        Posts:     posts,
        Formatted: formatted,
    }, nil
}

Manual error handling at every step, repetitive context threading, and imperative style.

ReaderResult approach:

func GetUserWithPosts(userID int) ReaderResult[UserWithPosts] {
    return F.Pipe3(
        Do(UserWithPosts{}),
        Bind(setUser, func(s UserWithPosts) ReaderResult[User] {
            return GetUser(userID)
        }),
        Bind(setPosts, func(s UserWithPosts) ReaderResult[[]Post] {
            return GetPosts(s.User.ID)
        }),
        Bind(setFormatted, func(s UserWithPosts) ReaderResult[string] {
            return FormatUser(s.User)
        }),
    )
}

Declarative pipeline, automatic error propagation, and clear data flow.

3. 🎨 Pure Composition - Side Effects Deferred

💡 Key Insight: ReaderResult separates building computations from executing them.

// Building the computation (pure, no side effects)
getUserPipeline := F.Pipe2(
    GetUser(123),
    Chain(func(user User) ReaderResult[[]Post] {
        return GetPosts(user.ID)
    }),
    Map(len[[]Post]),
)

// Execution happens later, at the edge of your system
postCount, err := getUserPipeline(ctx)

Benefits:

  • Computations can be built, tested, and reasoned about without executing side effects
  • Easy to mock and test individual components
  • Clear separation between business logic and execution
  • Computations are reusable with different contexts
4. 🧪 Improved Testability

Traditional approach:

func TestGetUserWithPosts(t *testing.T) {
    // Need to mock database, HTTP clients, etc.
    // Tests are tightly coupled to implementation
    ctx := context.Background()
    result, err := GetUserWithPosts(ctx, 123)
    // ...
}

ReaderResult approach:

func TestGetUserWithPosts(t *testing.T) {
    // Test the composition logic without executing side effects
    pipeline := GetUserWithPosts(123)
    
    // Can test with a mock context that provides test data
    testCtx := context.WithValue(context.Background(), "test", true)
    result, err := pipeline(testCtx)
    
    // Or test individual components in isolation
    mockGetUser := func(id int) ReaderResult[User] {
        return Of(User{ID: id, Name: "Test User"})
    }
}

You can test the composition logic separately from the actual I/O operations.

5. 📝 Better Error Context Accumulation

ReaderResult makes it easy to add context to errors as they propagate:

getUserWithContext := F.Pipe2(
    GetUser(userID),
    MapError(func(err error) error {
        return fmt.Errorf("failed to get user %d: %w", userID, err)
    }),
    Chain(func(user User) ReaderResult[UserWithPosts] {
        return F.Pipe1(
            GetPosts(user.ID),
            MapError(func(err error) error {
                return fmt.Errorf("failed to get posts for user %s: %w", user.Name, err)
            }),
            Map(func(posts []Post) UserWithPosts {
                return UserWithPosts{User: user, Posts: posts}
            }),
        )
    }),
)

Errors automatically accumulate context as they bubble up through the composition.

6. ⚡ Natural Parallel Execution

With applicative functors, independent operations can be expressed naturally:

// These operations don't depend on each other
getUserData := F.Pipe2(
    Do(UserData{}),
    ApS(setUser, GetUser(userID)),      // Can run in parallel
    ApS(setSettings, GetSettings(userID)), // Can run in parallel
    ApS(setPreferences, GetPreferences(userID)), // Can run in parallel
)

The structure makes it clear which operations are independent, enabling potential optimization.

7. 🔄 Retry and Recovery Patterns

ReaderResult makes retry logic composable. For production use, leverage the built-in Retrying function with configurable retry policies:

import (
    "time"
    R "github.com/IBM/fp-go/v2/retry"
)

// Production-ready retry with exponential backoff and context awareness
func WithRetry[A any](operation ReaderResult[A]) ReaderResult[A] {
    // Create a retry policy: exponential backoff with a cap, limited to 5 retries
    policy := R.Monoid.Concat(
        R.LimitRetries(5),
        R.CapDelay(10*time.Second, R.ExponentialBackoff(100*time.Millisecond)),
    )
    
    // Retry on any error (context cancellation is automatically handled)
    shouldRetry := func(val A, err error) bool {
        return err != nil
    }
    
    // Use built-in Retrying with automatic context cancellation support
    return Retrying(policy, R.Always(operation), shouldRetry)
}

// Use it:
reliableGetUser := WithRetry(GetUser(userID))
user, err := reliableGetUser(ctx) // Automatically retries with exponential backoff

// Or for simple cases, implement custom retry logic:
func SimpleRetry[A any](maxAttempts int, operation ReaderResult[A]) ReaderResult[A] {
    return func(ctx context.Context) (A, error) {
        var lastErr error
        for i := 0; i < maxAttempts; i++ {
            result, err := operation(ctx)
            if err == nil {
                return result, nil
            }
            lastErr = err
            time.Sleep(time.Second * time.Duration(i+1))
        }
        return *new(A), fmt.Errorf("failed after %d attempts: %w", maxAttempts, lastErr)
    }
}
8. 🎭 Middleware/Aspect-Oriented Programming

Cross-cutting concerns can be added as higher-order functions:

import (
    "time"
    F "github.com/IBM/fp-go/v2/function"
    RR "github.com/IBM/fp-go/v2/idiomatic/context/readerresult"
)

// Logging middleware
func WithLogging[A any](name string) RR.Operator[A, A] {
    return func(operation RR.ReaderResult[A]) RR.ReaderResult[A] {
        return func(ctx context.Context) (A, error) {
            log.Printf("Starting %s", name)
            start := time.Now()
            result, err := operation(ctx)
            log.Printf("Finished %s in %v (error: %v)", name, time.Since(start), err)
            return result, err
        }
    }
}

// Compose middleware using built-in functions:
robustGetUser := F.Pipe3(
    GetUser(userID),
    WithLogging[User]("GetUser"),
    RR.WithTimeout[User](5 * time.Second),  // Built-in timeout support
    WithRetry[User],
)

Built-in Middleware Functions:

  • WithTimeout - Add timeout to operations
  • WithDeadline - Add absolute deadline to operations
  • Local - Transform context for specific operations
9. 🛡️ Resource Management with Bracket

Safe resource handling with guaranteed cleanup:

readFile := Bracket(
    // Acquire
    func() ReaderResult[*os.File] {
        return func(ctx context.Context) (*os.File, error) {
            return os.Open("data.txt")
        }
    },
    // Use
    func(file *os.File) ReaderResult[string] {
        return func(ctx context.Context) (string, error) {
            data, err := io.ReadAll(file)
            return string(data), err
        }
    },
    // Release (always called)
    func(file *os.File, content string, err error) ReaderResult[any] {
        return func(ctx context.Context) (any, error) {
            return nil, file.Close()
        }
    },
)

The bracket pattern ensures resources are always cleaned up, even on errors.

10. 🔒 Type-Safe State Threading

Do-notation provides type-safe accumulation of state:

type UserProfile struct {
    User     User
    Posts    []Post
    Comments []Comment
    Stats    Statistics
}

buildProfile := F.Pipe4(
    Do(UserProfile{}),
    Bind(setUser, func(s UserProfile) ReaderResult[User] {
        return GetUser(userID)
    }),
    Bind(setPosts, func(s UserProfile) ReaderResult[[]Post] {
        return GetPosts(s.User.ID) // Can access previous results
    }),
    Bind(setComments, func(s UserProfile) ReaderResult[[]Comment] {
        return GetComments(s.User.ID)
    }),
    Bind(setStats, func(s UserProfile) ReaderResult[Statistics] {
        return CalculateStats(s.Posts, s.Comments) // Can use multiple previous results
    }),
)

The compiler ensures you can't access fields that haven't been set yet.

🔍 Using Optics with Bind and BindTo

Optics provide powerful, composable abstractions for working with data structures. They integrate seamlessly with ReaderResult's Bind and BindTo methods, enabling elegant state accumulation patterns.

Lenses for Product Types (Structs)

Lenses focus on struct fields and can be used as setters in Bind operations:

import (
    "github.com/IBM/fp-go/v2/optics/lens"
    F "github.com/IBM/fp-go/v2/function"
)

type User struct {
    ID   int
    Name string
}

type UserProfile struct {
    User     User
    Posts    []Post
    Comments []Comment
}

// Auto-generated or manually created lenses
var (
    userLens = lens.MakeLens(
        func(p UserProfile) User { return p.User },
        func(p UserProfile, u User) UserProfile {
            p.User = u
            return p
        },
    )
    postsLens = lens.MakeLens(
        func(p UserProfile) []Post { return p.Posts },
        func(p UserProfile, posts []Post) UserProfile {
            p.Posts = posts
            return p
        },
    )
    commentsLens = lens.MakeLens(
        func(p UserProfile) []Comment { return p.Comments },
        func(p UserProfile, comments []Comment) UserProfile {
            p.Comments = comments
            return p
        },
    )
    
    // Lens for User.ID field
    userIDLens = lens.MakeLens(
        func(u User) int { return u.ID },
        func(u User, id int) User {
            u.ID = id
            return u
        },
    )
    
    // Composed lens: UserProfile -> User -> ID
    // This demonstrates lens composition - a key benefit of optics!
    profileUserIDLens = F.Pipe1(
        userLens,
        lens.Compose[UserProfile](userIDLens),
    )
)

// Use lenses as setters in Bind
buildProfile := F.Pipe3(
    Do(UserProfile{}),
    Bind(userLens.Set, func(s UserProfile) ReaderResult[User] {
        return GetUser(userID)
    }),
    Bind(postsLens.Set, func(s UserProfile) ReaderResult[[]Post] {
        // Use composed lens to access nested User.ID directly
        return GetPosts(profileUserIDLens.Get(s))
    }),
    Bind(commentsLens.Set, func(s UserProfile) ReaderResult[[]Comment] {
        // Composed lens makes nested access clean and type-safe
        return GetComments(profileUserIDLens.Get(s))
    }),
)

Benefits:

  • Type-safe field updates
  • Reusable lens definitions
  • Clear separation of data access from business logic
  • Can be auto-generated with go generate (see optics documentation)
Prisms for Sum Types (Variants)

Prisms are particularly powerful in Bind operations as they act as generalized constructors. The prism's ReverseGet function constructs values of sum types, making them ideal for building up complex results:

import (
    "github.com/IBM/fp-go/v2/optics/prism"
    O "github.com/IBM/fp-go/v2/option"
)

type APIResponse struct {
    UserData   O.Option[User]
    PostsData  O.Option[[]Post]
    StatsData  O.Option[Stats]
}

// Prisms for Option fields - ReverseGet acts as a constructor
var (
    userDataPrism = prism.MakePrism(
        func(r APIResponse) O.Option[User] { return r.UserData },
        func(u User) APIResponse {
            return APIResponse{UserData: O.Some(u)}
        },
    )
    postsDataPrism = prism.MakePrism(
        func(r APIResponse) O.Option[[]Post] { return r.PostsData },
        func(posts []Post) APIResponse {
            return APIResponse{PostsData: O.Some(posts)}
        },
    )
)

// Use prisms to construct and accumulate optional data
fetchAPIData := F.Pipe3(
    Do(APIResponse{}),
    // ReverseGet constructs APIResponse with UserData set
    Bind(userDataPrism.ReverseGet, func(s APIResponse) ReaderResult[User] {
        return GetUser(userID)
    }),
    // ReverseGet constructs APIResponse with PostsData set
    Bind(postsDataPrism.ReverseGet, func(s APIResponse) ReaderResult[[]Post] {
        return O.Fold(
            func() ReaderResult[[]Post] { return Of([]Post{}) },
            func(user User) ReaderResult[[]Post] { return GetPosts(user.ID) },
        )(s.UserData)
    }),
    // Handle optional stats
    BindTo(func(s APIResponse) ReaderResult[APIResponse] {
        return F.Pipe1(
            GetStats(userID),
            Map(func(stats Stats) APIResponse {
                s.StatsData = O.Some(stats)
                return s
            }),
        )
    }),
)

Why Prisms Excel in Bind:

  • Generalized Constructors: ReverseGet creates values from variants, perfect for building sum types
  • Partial Construction: Build complex structures incrementally
  • Type Safety: Compiler ensures correct variant handling
  • Composability: Prisms compose naturally with monadic operations
Combining Lenses and Prisms

For maximum flexibility, combine both optics in a single pipeline:

type ComplexState struct {
    Config   Config
    Result   O.Option[ProcessingResult]
    Metadata Metadata
}

var (
    configLens = lens.MakeLens(
        func(s ComplexState) Config { return s.Config },
        func(s ComplexState, c Config) ComplexState {
            s.Config = c
            return s
        },
    )
    resultPrism = prism.MakePrism(
        func(s ComplexState) O.Option[ProcessingResult] { return s.Result },
        func(r ProcessingResult) ComplexState {
            return ComplexState{Result: O.Some(r)}
        },
    )
)

pipeline := F.Pipe3(
    Do(ComplexState{}),
    Bind(configLens.Set, LoadConfig),           // Lens for required field
    Bind(resultPrism.ReverseGet, ProcessData),  // Prism for optional result
    Bind(metadataLens.Set, ExtractMetadata),    // Lens for metadata
)

Learn More:

11. ⏱️ Automatic Context Cancellation Checks

ReaderResult automatically checks for context cancellation at composition boundaries through WithContextK, ensuring fail-fast behavior without manual checks:

Traditional approach:

func ProcessData(ctx context.Context, data Data) (Result, error) {
    // Manual cancellation check
    if ctx.Err() != nil {
        return Result{}, ctx.Err()
    }
    
    step1, err := Step1(ctx, data)
    if err != nil {
        return Result{}, err
    }
    
    // Manual cancellation check again
    if ctx.Err() != nil {
        return Result{}, ctx.Err()
    }
    
    step2, err := Step2(ctx, step1)
    if err != nil {
        return Result{}, err
    }
    
    // And again...
    if ctx.Err() != nil {
        return Result{}, ctx.Err()
    }
    
    return Step3(ctx, step2)
}

ReaderResult approach:

func ProcessData(data Data) ReaderResult[Result] {
    return F.Pipe3(
        Step1(data),
        Chain(Step2),  // Automatic cancellation check before Step2
        Chain(Step3),  // Automatic cancellation check before Step3
    )
}

How it works:

  • All Bind operations use WithContextK internally
  • WithContextK wraps each Kleisli arrow with a cancellation check
  • Before executing each step, it checks ctx.Err() and fails fast if cancelled
  • No manual cancellation checks needed in your business logic
  • Ensures long-running pipelines respect context cancellation at every step

Example with timeout:

pipeline := F.Pipe3(
    FetchUser(userID),        // Step 1
    Chain(FetchPosts),        // Cancellation checked before Step 2
    Chain(EnrichWithMetadata), // Cancellation checked before Step 3
)

// Set a timeout
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()

// If Step 1 takes too long, Steps 2 and 3 won't execute
result, err := pipeline(ctx)

This makes ReaderResult ideal for:

  • Long-running pipelines that should respect timeouts
  • Operations that need to be cancellable at any point
  • Composing third-party functions that don't check context themselves
  • Building responsive services that handle request cancellation properly

🎯 When to Use ReaderResult

✅ Use ReaderResult when:

  • You have complex composition of context-dependent operations
  • You want to separate business logic from execution
  • You need better testability and mockability
  • You want declarative, pipeline-style code
  • You need to add cross-cutting concerns (logging, retry, timeout)
  • You want type-safe state accumulation
  • You need automatic context cancellation checks at composition boundaries
  • You're building long-running pipelines that should respect timeouts

❌ Stick with traditional Go when:

  • You have simple, one-off operations
  • The team is unfamiliar with functional patterns
  • You're writing library code that needs to be idiomatic Go
  • Performance is absolutely critical (though the overhead is minimal)

🚀 Quick Start

import (
    "context"
    F "github.com/IBM/fp-go/v2/function"
    RR "github.com/IBM/fp-go/v2/idiomatic/context/readerresult"
)

// Define your operations
func GetUser(id int) RR.ReaderResult[User] {
    return func(ctx context.Context) (User, error) {
        // Your implementation
    }
}

// Compose them
pipeline := F.Pipe2(
    GetUser(123),
    RR.Chain(func(user User) RR.ReaderResult[[]Post] {
        return GetPosts(user.ID)
    }),
    RR.Map(CreateSummary),
)

// Execute at the edge
summary, err := pipeline(context.Background())

Key functions used:

  • Chain - Sequence dependent operations
  • Map - Transform success values

🔄 Converting Traditional Go Functions

ReaderResult provides convenient functions to convert traditional Go functions (that take context.Context as their first parameter) into functional ReaderResult operations. This makes it easy to integrate existing code into functional pipelines.

Using FromXXX Functions (Uncurried)

The From0, From1, From2, From3 functions convert traditional Go functions into ReaderResult-returning functions that take all parameters at once. This is the most straightforward conversion for direct use.

// Traditional Go function
func getUser(ctx context.Context, id int) (User, error) {
    // ... database query
    return User{ID: id, Name: "Alice"}, nil
}

func updateUser(ctx context.Context, id int, name string) (User, error) {
    // ... database update
    return User{ID: id, Name: name}, nil
}

// Convert using From1 (1 parameter besides context)
getUserRR := RR.From1(getUser)

// Convert using From2 (2 parameters besides context)
updateUserRR := RR.From2(updateUser)

// Use in a pipeline
pipeline := F.Pipe2(
    getUserRR(123),                    // Returns ReaderResult[User]
    RR.Chain(func(user User) RR.ReaderResult[User] {
        return updateUserRR(user.ID, "Bob")  // All params at once
    }),
)

result, err := pipeline(ctx)

Available From functions:

  • From0: Converts func(context.Context) (A, error)func() ReaderResult[A]
  • From1: Converts func(context.Context, T1) (A, error)func(T1) ReaderResult[A]
  • From2: Converts func(context.Context, T1, T2) (A, error)func(T1, T2) ReaderResult[A]
  • From3: Converts func(context.Context, T1, T2, T3) (A, error)func(T1, T2, T3) ReaderResult[A]
Using CurryXXX Functions (Curried)

The Curry0, Curry1, Curry2, Curry3 functions convert traditional Go functions into curried ReaderResult-returning functions. This enables partial application, which is useful for building reusable function pipelines.

// Traditional Go function
func createPost(ctx context.Context, userID int, title string, body string) (Post, error) {
    return Post{UserID: userID, Title: title, Body: body}, nil
}

// Convert using Curry3 (3 parameters besides context)
createPostRR := RR.Curry3(createPost)

// Partial application - build specialized functions
createPostForUser42 := createPostRR(42)
createPostWithTitle := createPostForUser42("My Title")

// Complete the application
rr := createPostWithTitle("Post body content")
post, err := rr(ctx)

// Or apply all at once
post2, err := createPostRR(42)("Another Title")("Another body")(ctx)

Available Curry functions:

  • Curry0: Converts func(context.Context) (A, error)ReaderResult[A]
  • Curry1: Converts func(context.Context, T1) (A, error)func(T1) ReaderResult[A]
  • Curry2: Converts func(context.Context, T1, T2) (A, error)func(T1) func(T2) ReaderResult[A]
  • Curry3: Converts func(context.Context, T1, T2, T3) (A, error)func(T1) func(T2) func(T3) ReaderResult[A]
Practical Example: Integrating Existing Code
// Existing traditional Go functions (e.g., from a database package)
func fetchUser(ctx context.Context, id int) (User, error) { /* ... */ }
func fetchPosts(ctx context.Context, userID int) ([]Post, error) { /* ... */ }
func fetchComments(ctx context.Context, postID int) ([]Comment, error) { /* ... */ }

// Convert them all to ReaderResult
var (
    GetUser     = RR.From1(fetchUser)
    GetPosts    = RR.From1(fetchPosts)
    GetComments = RR.From1(fetchComments)
)

// Now compose them functionally
func GetUserWithData(userID int) RR.ReaderResult[UserData] {
    return F.Pipe3(
        GetUser(userID),
        RR.Chain(func(user User) RR.ReaderResult[[]Post] {
            return GetPosts(user.ID)
        }),
        RR.Map(func(posts []Post) UserData {
            return UserData{
                User:  user,
                Posts: posts,
            }
        }),
    )
}

// Execute
userData, err := GetUserWithData(123)(ctx)
When to Use From vs Curry

Use FromXXX when:

  • You want straightforward conversion for immediate use
  • You're calling functions with all parameters at once
  • You prefer a more familiar, uncurried style
  • You're converting functions for one-time use in a pipeline

Use CurryXXX when:

  • You want to partially apply parameters
  • You're building reusable, specialized functions
  • You need maximum composability
  • You're working with higher-order functions that expect curried inputs
Converting Back: Uncurry Functions

If you need to convert a ReaderResult function back to traditional Go style (e.g., for interfacing with non-functional code), use the Uncurry1, Uncurry2, Uncurry3 functions:

// Functional style
getUserRR := func(id int) RR.ReaderResult[User] {
    return func(ctx context.Context) (User, error) {
        return User{ID: id}, nil
    }
}

// Convert back to traditional Go
getUser := RR.Uncurry1(getUserRR)

// Now callable in traditional style
user, err := getUser(ctx, 123)

📚 API Reference

Core Functions
  • Map - Transform the success value
  • Chain - Sequence operations (also known as FlatMap or Bind)
  • Bind - Do-notation binding for state accumulation
  • Do - Start a Do-notation pipeline
  • ApS - Applicative sequencing for parallel operations
  • Of - Lift a pure value into ReaderResult
Resource Management
  • Bracket - Safe resource acquisition and cleanup
Conversion Functions
Error Handling
  • MapError - Transform error values
  • OrElse - Provide fallback on error
Full Documentation

📚 See Also

Documentation

Overview

Package readerresult provides a ReaderResult monad specialized for context.Context.

A ReaderResult[A] represents an effectful computation that:

  • Takes a context.Context as input
  • May fail with an error (Result aspect, which is Either[error, A])
  • Returns a value of type A on success

The type is defined as: ReaderResult[A any] = func(context.Context) (A, error)

This is equivalent to Reader[context.Context, Result[A]] or Reader[context.Context, Either[error, A]], but specialized to always use context.Context as the environment type.

Effectful Computations with Context

ReaderResult is particularly well-suited for representing effectful computations in Go. An effectful computation is one that:

  • Performs side effects (I/O, network calls, database operations, etc.)
  • May fail with an error
  • Requires contextual information (cancellation, deadlines, request-scoped values)

By using context.Context as the fixed environment type, ReaderResult[A] provides:

  1. Cancellation propagation - operations can be cancelled via context
  2. Deadline/timeout handling - operations respect context deadlines
  3. Request-scoped values - access to request metadata, trace IDs, etc.
  4. Functional composition - chain effectful operations while maintaining context
  5. Error handling - explicit error propagation through the Result type

This pattern is idiomatic in Go, where functions performing I/O conventionally accept context.Context as their first parameter: func(ctx context.Context, ...) (Result, error). ReaderResult preserves this convention while enabling functional composition.

Example of an effectful computation:

// An effectful operation that queries a database
func fetchUser(ctx context.Context, id int) (User, error) {
    // ctx provides cancellation, deadlines, and request context
    row := db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = ?", id)
    var user User
    err := row.Scan(&user.ID, &user.Name)
    return user, err
}

// Lift into ReaderResult for functional composition
getUser := readerresult.Curry1(fetchUser)

// Compose multiple effectful operations
pipeline := F.Pipe2(
    getUser(42),  // ReaderResult[User]
    readerresult.Chain(func(user User) readerresult.ReaderResult[[]Post] {
        return getPosts(user.ID)  // Another effectful operation
    }),
)

// Execute with a context (e.g., from an HTTP request)
ctx := r.Context()  // HTTP request context
posts, err := pipeline(ctx)

Use Cases

ReaderResult is particularly useful for:

  1. Effectful computations with context - operations that perform I/O and need cancellation/deadlines
  2. Functional error handling - compose operations that depend on context and may error
  3. Testing - easily mock context-dependent operations
  4. HTTP handlers - chain request processing operations with proper context propagation

Composition

ReaderResult provides several ways to compose computations:

  1. Map - transform successful values
  2. Chain (FlatMap) - sequence dependent operations
  3. Ap - combine independent computations
  4. Do-notation - imperative-style composition with Bind

Do-Notation Example

type State struct {
    User   User
    Posts  []Post
}

result := F.Pipe2(
    readerresult.Do(State{}),
    readerresult.Bind(
        func(user User) func(State) State {
            return func(s State) State { s.User = user; return s }
        },
        func(s State) readerresult.ReaderResult[User] {
            return getUser(42)
        },
    ),
    readerresult.Bind(
        func(posts []Post) func(State) State {
            return func(s State) State { s.Posts = posts; return s }
        },
        func(s State) readerresult.ReaderResult[[]Post] {
            return getPosts(s.User.ID)
        },
    ),
)

Currying Functions with Context

The Curry functions enable partial application of function parameters while deferring the context.Context parameter until execution time.

When you curry a function like func(context.Context, T1, T2) (A, error), the context.Context becomes the last argument to be applied, even though it appears first in the original function signature. This is intentional and follows Go's context-first convention while enabling functional composition patterns.

Why context.Context is the last curried argument:

  • In Go, context conventionally comes first: func(ctx context.Context, params...) (Result, error)
  • In curried form: Curry2(f)(param1)(param2) returns ReaderResult[A]
  • The ReaderResult is then applied to ctx: Curry2(f)(param1)(param2)(ctx)
  • This allows partial application of business parameters before providing the context

Example with database operations:

// Database operations following Go conventions (context first)
func fetchUser(ctx context.Context, db *sql.DB, id int) (User, error) {
    row := db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = ?", id)
    var user User
    err := row.Scan(&user.ID, &user.Name)
    return user, err
}

func updateUser(ctx context.Context, db *sql.DB, id int, name string) (User, error) {
    _, err := db.ExecContext(ctx, "UPDATE users SET name = ? WHERE id = ?", name, id)
    if err != nil {
        return User{}, err
    }
    return fetchUser(ctx, db, id)
}

// Curry these into composable operations
getUser := readerresult.Curry2(fetchUser)
updateUserName := readerresult.Curry3(updateUser)

// Compose operations with partial application
pipeline := F.Pipe2(
    getUser(db)(42),  // ReaderResult[User] - db and id applied, waiting for ctx
    readerresult.Chain(func(user User) readerresult.ReaderResult[User] {
        newName := user.Name + " (updated)"
        return updateUserName(db)(user.ID)(newName)  // Waiting for ctx
    }),
)

// Execute by providing the context
ctx := context.Background()
updatedUser, err := pipeline(ctx)

The key insight is that currying creates a chain where:

  1. Business parameters are applied first: getUser(db)(42)
  2. This returns a ReaderResult[User] that waits for the context
  3. Multiple operations can be composed before providing the context
  4. Finally, the context is provided to execute everything: pipeline(ctx)

This pattern is particularly useful for:

  • Creating reusable operation pipelines independent of specific contexts
  • Testing with different contexts (with timeouts, cancellation, etc.)
  • Composing operations that share the same context
  • Deferring context creation until execution time

Error Handling

ReaderResult provides several functions for error handling:

  • Left/Right - create failed/successful values
  • GetOrElse - provide a default value for errors
  • OrElse - recover from errors with an alternative computation
  • Fold - handle both success and failure cases
  • ChainEitherK - lift result.Result computations into ReaderResult

Relationship to Other Monads

ReaderResult is related to several other monads in this library:

  • Reader[context.Context, A] - ReaderResult without error handling
  • Result[A] (Either[error, A]) - error handling without context dependency
  • IOResult[A] - similar to ReaderResult but without explicit context parameter
  • ReaderIOResult[R, A] - generic version that allows custom environment type R

Performance Note

ReaderResult is a zero-cost abstraction - it compiles to a simple function type with no runtime overhead beyond the underlying computation.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func ApResultS

func ApResultS[
	S1, S2, T any](
	setter func(T) func(S1) S2,
) func(T, error) Operator[S1, S2]

ApResultS applies an idiomatic Go tuple (value, error) in applicative style.

IMPORTANT: The (Value, error) tuple is PURE (side-effect free) - it doesn't depend on context. Use this for pure Go error-handling results.

Example

ExampleApResultS demonstrates applying an idiomatic Go tuple (value, error) in applicative style, combining it with the current state in a do-notation chain.

// Simulate an idiomatic Go function result
value, err := "Hello, World!", error(nil)

result := F.Pipe1(
	Do(MessageState{}),
	func(rr ReaderResult[MessageState]) ReaderResult[MessageState] {
		return F.Pipe1(
			rr,
			ApResultS(
				func(msg string) Endomorphism[MessageState] {
					return func(s MessageState) MessageState {
						s.Message = msg
						return s
					}
				},
			)(value, err),
		)
	},
)

state, resultErr := result(context.Background())
fmt.Printf("Message: %s, Error: %v\n", state.Message, resultErr)
Output:

Message: Hello, World!, Error: <nil>

func BindToEither

func BindToEither[
	S1, T any](
	setter func(T) S1,
) func(Result[T]) ReaderResult[S1]

BindToEither converts a Result (Either) into a ReaderResult and binds it to create an initial state.

IMPORTANT: Result[T] is PURE (side-effect free) - it doesn't depend on context. Use this to lift pure error-handling values into the ReaderResult context.

Example

ExampleBindToEither demonstrates converting a Result (Either) into a ReaderResult and binding it to create an initial state.

// A Result value
resultValue := RES.Of(100)

result := F.Pipe1(
	resultValue,
	BindToEither(func(v int) ValueState {
		return ValueState{Value: v}
	}),
)

state, err := result(context.Background())
fmt.Printf("Value: %d, Error: %v\n", state.Value, err)
Output:

Value: 100, Error: <nil>

func BindToReader

func BindToReader[
	S1, T any](
	setter func(T) S1,
) func(Reader[context.Context, T]) ReaderResult[S1]

BindToReader converts a Reader computation into a ReaderResult and binds it to create an initial state.

IMPORTANT: Reader[Context, T] is EFFECTFUL because it depends on context.Context. Use this when you have a context-dependent computation that cannot fail.

Example

ExampleBindToReader demonstrates converting a Reader computation into a ReaderResult and binding it to create an initial state.

// A Reader that extracts request ID from context
getRequestID := func(ctx context.Context) string {
	if val := ctx.Value("requestID"); val != nil {
		return val.(string)
	}
	return "unknown"
}

result := F.Pipe1(
	getRequestID,
	BindToReader(func(id string) RequestState {
		return RequestState{RequestID: id}
	}),
)

ctx := context.WithValue(context.Background(), "requestID", "req-123")
state, err := result(ctx)
fmt.Printf("Request ID: %s, Error: %v\n", state.RequestID, err)
Output:

Request ID: req-123, Error: <nil>

func BindToResult

func BindToResult[
	S1, T any](
	setter func(T) S1,
) func(T, error) ReaderResult[S1]

BindToResult converts an idiomatic Go tuple (value, error) into a ReaderResult and binds it to create an initial state.

IMPORTANT: The (Value, error) tuple is PURE (side-effect free) - it doesn't depend on context. Use this to lift pure Go error-handling results into the ReaderResult context.

Example

ExampleBindToResult demonstrates converting an idiomatic Go tuple (value, error) into a ReaderResult and binding it to create an initial state.

// Simulate an idiomatic Go function result
value, err := "success", error(nil)

result := F.Pipe1(
	BindToResult(func(v string) ResultState {
		return ResultState{Result: v}
	}),
	func(f func(string, error) ReaderResult[ResultState]) ReaderResult[ResultState] {
		return f(value, err)
	},
)

state, resultErr := result(context.Background())
fmt.Printf("Result: %s, Error: %v\n", state.Result, resultErr)
Output:

Result: success, Error: <nil>

func ChainOptionK

func ChainOptionK[A, B any](onNone Lazy[error]) func(option.Kleisli[A, B]) Operator[A, B]

func Curry1

func Curry1[T1, A any](f func(context.Context, T1) (A, error)) func(T1) ReaderResult[A]

Curry1 converts a function with one parameter into a curried ReaderResult-returning function.

The context.Context parameter is handled by the ReaderResult, allowing you to partially apply the business parameter before providing the context.

Type Parameters:

  • T1: The first parameter type
  • A: The return value type

Parameters:

  • f: A function that takes (context.Context, T1) and returns (A, error)

Returns:

  • A curried function that takes T1 and returns ReaderResult[A]

Example:

func getUser(ctx context.Context, id int) (User, error) {
    // ... implementation
    return user, nil
}
getUserRR := readerresult.Curry1(getUser)
rr := getUserRR(42)  // Partially applied
user, err := rr(ctx)  // Execute with context

func Curry2

func Curry2[T1, T2, A any](f func(context.Context, T1, T2) (A, error)) func(T1) func(T2) ReaderResult[A]

Curry2 converts a function with two parameters into a curried ReaderResult-returning function.

The context.Context parameter is handled by the ReaderResult, allowing you to partially apply the business parameters before providing the context.

Type Parameters:

  • T1: The first parameter type
  • T2: The second parameter type
  • A: The return value type

Parameters:

  • f: A function that takes (context.Context, T1, T2) and returns (A, error)

Returns:

  • A curried function that takes T1, then T2, and returns ReaderResult[A]

Example:

func updateUser(ctx context.Context, id int, name string) (User, error) {
    // ... implementation
    return user, nil
}
updateUserRR := readerresult.Curry2(updateUser)
rr := updateUserRR(42)("Alice")  // Partially applied
user, err := rr(ctx)  // Execute with context

func Curry3

func Curry3[T1, T2, T3, A any](f func(context.Context, T1, T2, T3) (A, error)) func(T1) func(T2) func(T3) ReaderResult[A]

Curry3 converts a function with three parameters into a curried ReaderResult-returning function.

The context.Context parameter is handled by the ReaderResult, allowing you to partially apply the business parameters before providing the context.

Type Parameters:

  • T1: The first parameter type
  • T2: The second parameter type
  • T3: The third parameter type
  • A: The return value type

Parameters:

  • f: A function that takes (context.Context, T1, T2, T3) and returns (A, error)

Returns:

  • A curried function that takes T1, then T2, then T3, and returns ReaderResult[A]

Example:

func createPost(ctx context.Context, userID int, title string, body string) (Post, error) {
    // ... implementation
    return post, nil
}
createPostRR := readerresult.Curry3(createPost)
rr := createPostRR(42)("Title")("Body")  // Partially applied
post, err := rr(ctx)  // Execute with context

func Fold

func Fold[A, B any](onLeft reader.Kleisli[context.Context, error, B], onRight reader.Kleisli[context.Context, A, B]) func(ReaderResult[A]) Reader[context.Context, B]

func From0

func From0[A any](f func(context.Context) (A, error)) func() ReaderResult[A]

From0 converts a context-taking function into a thunk that returns a ReaderResult.

Unlike Curry0 which returns a ReaderResult directly, From0 returns a function that when called produces a ReaderResult. This is useful for lazy evaluation.

Type Parameters:

  • A: The return value type

Parameters:

  • f: A function that takes context.Context and returns (A, error)

Returns:

  • A thunk (function with no parameters) that returns ReaderResult[A]

Example:

func getConfig(ctx context.Context) (Config, error) {
    return Config{Port: 8080}, nil
}
thunk := readerresult.From0(getConfig)
rr := thunk()  // Create the ReaderResult
config, err := rr(ctx)  // Execute it

func From1

func From1[T1, A any](f func(context.Context, T1) (A, error)) func(T1) ReaderResult[A]

From1 converts a function with one parameter into an uncurried ReaderResult-returning function.

Unlike Curry1 which returns a curried function, From1 returns a function that takes all parameters at once (except context). This is more convenient for direct calls.

Type Parameters:

  • T1: The parameter type
  • A: The return value type

Parameters:

  • f: A function that takes (context.Context, T1) and returns (A, error)

Returns:

  • A function that takes T1 and returns ReaderResult[A]

Example:

func getUser(ctx context.Context, id int) (User, error) {
    return User{ID: id}, nil
}
getUserRR := readerresult.From1(getUser)
rr := getUserRR(42)
user, err := rr(ctx)

func From2

func From2[T1, T2, A any](f func(context.Context, T1, T2) (A, error)) func(T1, T2) ReaderResult[A]

From2 converts a function with two parameters into an uncurried ReaderResult-returning function.

Type Parameters:

  • T1: The first parameter type
  • T2: The second parameter type
  • A: The return value type

Parameters:

  • f: A function that takes (context.Context, T1, T2) and returns (A, error)

Returns:

  • A function that takes (T1, T2) and returns ReaderResult[A]

Example:

func updateUser(ctx context.Context, id int, name string) (User, error) {
    return User{ID: id, Name: name}, nil
}
updateUserRR := readerresult.From2(updateUser)
rr := updateUserRR(42, "Alice")
user, err := rr(ctx)

func From3

func From3[T1, T2, T3, A any](f func(context.Context, T1, T2, T3) (A, error)) func(T1, T2, T3) ReaderResult[A]

From3 converts a function with three parameters into an uncurried ReaderResult-returning function.

Type Parameters:

  • T1: The first parameter type
  • T2: The second parameter type
  • T3: The third parameter type
  • A: The return value type

Parameters:

  • f: A function that takes (context.Context, T1, T2, T3) and returns (A, error)

Returns:

  • A function that takes (T1, T2, T3) and returns ReaderResult[A]

Example:

func createPost(ctx context.Context, userID int, title, body string) (Post, error) {
    return Post{UserID: userID, Title: title, Body: body}, nil
}
createPostRR := readerresult.From3(createPost)
rr := createPostRR(42, "Title", "Body")
post, err := rr(ctx)

func GetOrElse

func GetOrElse[A any](onLeft reader.Kleisli[context.Context, error, A]) func(ReaderResult[A]) Reader[context.Context, A]

func Read

func Read[A any](ctx context.Context) func(ReaderResult[A]) (A, error)

Read executes a ReaderResult by providing it with a context.Context.

This is the elimination form for ReaderResult - it "runs" the computation by supplying the required environment, producing a (value, error) tuple.

Type Parameters:

  • A: The result value type

Parameters:

  • ctx: The context.Context environment to provide

Returns:

  • A function that executes a ReaderResult[A] and returns (A, error)

func ToReaderResult

func ToReaderResult[A any](r ReaderResult[A]) RS.ReaderResult[A]

func TraverseReader

func TraverseReader[R, A, B any](
	f reader.Kleisli[R, A, B],
) func(ReaderResult[A]) Kleisli[R, B]

TraverseReader combines SequenceReader with a Kleisli arrow transformation.

It takes a Reader Kleisli arrow (a function from A to Reader[R, B]) and returns a function that transforms ReaderResult[A] into a Kleisli arrow from context.Context and R to B. This is useful for transforming values within a ReaderResult while introducing an additional Reader dependency.

Type Parameters:

  • R: The Reader's environment type
  • A: The input type
  • B: The output type

Parameters:

  • f: A Kleisli arrow that transforms A into Reader[R, B]

Returns:

  • A function that transforms ReaderResult[A] into a Kleisli arrow from context.Context and R to B

Example:

type Config struct {
    Multiplier int
}

// A Kleisli arrow that uses Config to transform int to string
formatWithConfig := func(n int) reader.Reader[Config, string] {
    return func(cfg Config) string {
        return fmt.Sprintf("Value: %d", n * cfg.Multiplier)
    }
}

// Create a ReaderResult[int]
getValue := readerresult.Of[int](42)

// Traverse: transform the int using the Reader Kleisli arrow
traversed := readerresult.TraverseReader[Config](formatWithConfig)(getValue)
result, err := traversed(ctx)(Config{Multiplier: 2})
// result == "Value: 84"

func Uncurry1

func Uncurry1[T1, A any](f func(T1) ReaderResult[A]) func(context.Context, T1) (A, error)

Uncurry1 converts a curried ReaderResult function back to a standard Go function.

This is the inverse of Curry1, useful when you need to call curried functions in a traditional Go style.

Type Parameters:

  • T1: The parameter type
  • A: The return value type

Parameters:

  • f: A curried function that takes T1 and returns ReaderResult[A]

Returns:

  • A function that takes (context.Context, T1) and returns (A, error)

Example:

curriedFn := func(id int) readerresult.ReaderResult[User] { ... }
normalFn := readerresult.Uncurry1(curriedFn)
user, err := normalFn(ctx, 42)

func Uncurry2

func Uncurry2[T1, T2, A any](f func(T1) func(T2) ReaderResult[A]) func(context.Context, T1, T2) (A, error)

Uncurry2 converts a curried ReaderResult function with two parameters back to a standard Go function.

This is the inverse of Curry2.

Type Parameters:

  • T1: The first parameter type
  • T2: The second parameter type
  • A: The return value type

Parameters:

  • f: A curried function that takes T1, then T2, and returns ReaderResult[A]

Returns:

  • A function that takes (context.Context, T1, T2) and returns (A, error)

func Uncurry3

func Uncurry3[T1, T2, T3, A any](f func(T1) func(T2) func(T3) ReaderResult[A]) func(context.Context, T1, T2, T3) (A, error)

Uncurry3 converts a curried ReaderResult function with three parameters back to a standard Go function.

This is the inverse of Curry3.

Type Parameters:

  • T1: The first parameter type
  • T2: The second parameter type
  • T3: The third parameter type
  • A: The return value type

Parameters:

  • f: A curried function that takes T1, then T2, then T3, and returns ReaderResult[A]

Returns:

  • A function that takes (context.Context, T1, T2, T3) and returns (A, error)

Types

type Either

type Either[E, A any] = either.Either[E, A]

Either represents a value that can be one of two types: Left (E) or Right (A).

type Endomorphism

type Endomorphism[A any] = endomorphism.Endomorphism[A]

Endomorphism represents a function from type A to type A.

type Kleisli

type Kleisli[A, B any] = Reader[A, ReaderResult[B]]

Kleisli represents a Kleisli arrow from A to ReaderResult[B]. It's a function that takes a value of type A and returns a computation that produces B or an error in a context.

func FromPredicate

func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) Kleisli[A, A]

func SequenceReader

func SequenceReader[R, A any](ma ReaderResult[Reader[R, A]]) Kleisli[R, A]

SequenceReader swaps the order of nested environment parameters when the inner type is a Reader.

It transforms ReaderResult[Reader[R, A]] into a function that takes context.Context first, then R, and returns (A, error). This is useful when you have a ReaderResult computation that produces a Reader, and you want to sequence the environment dependencies.

Type Parameters:

  • R: The inner Reader's environment type
  • A: The final result type

Parameters:

  • ma: A ReaderResult that produces a Reader[R, A]

Returns:

  • A Kleisli arrow that takes context.Context and R to produce (A, error)

Example:

type Config struct {
    DatabaseURL string
}

// Returns a ReaderResult that produces a Reader
getDBReader := func(ctx context.Context) (reader.Reader[Config, string], error) {
    return func(cfg Config) string {
        return cfg.DatabaseURL
    }, nil
}

// Sequence the environments: context.Context -> Config -> string
sequenced := readerresult.SequenceReader[Config, string](getDBReader)
result, err := sequenced(ctx)(config)

func TraverseArray

func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B]

TraverseArray applies a ReaderResult-returning function to each element of an array, collecting the results. If any element fails, the entire operation fails with the first error.

Example:

parseUser := func(id int) readerresult.ReaderResult[DB, User] { ... }
ids := []int{1, 2, 3}
result := readerresult.TraverseArray[DB](parseUser)(ids)
// result(db) returns ([]User, nil) with all users or (nil, error) on first error

func TraverseArrayWithIndex

func TraverseArrayWithIndex[A, B any](f func(int, A) ReaderResult[B]) Kleisli[[]A, []B]

TraverseArrayWithIndex is like TraverseArray but the function also receives the element's index. This is useful when the transformation depends on the position in the array.

Example:

processItem := func(idx int, item string) readerresult.ReaderResult[Config, int] {
    return readerresult.Of[Config](idx + len(item))
}
items := []string{"a", "bb", "ccc"}
result := readerresult.TraverseArrayWithIndex[Config](processItem)(items)

func WithCloser

func WithCloser[B any, A io.Closer](onCreate Lazy[ReaderResult[A]]) Kleisli[Kleisli[A, B], B]

WithCloser creates a higher-order function for managing resources that implement io.Closer.

This is a specialized version of WithResource that automatically handles cleanup for any resource implementing the io.Closer interface (such as files, network connections, HTTP response bodies, etc.). It eliminates the need to manually specify the release function, making it more convenient for common Go resources.

The function automatically calls Close() on the resource when the operation completes, regardless of success or failure. This ensures proper resource cleanup following Go's standard io.Closer pattern.

Type Parameters:

  • B: The type of the result produced by using the resource
  • A: The type of the resource, which must implement io.Closer

Parameters:

  • onCreate: Lazy computation that creates/acquires the io.Closer resource

Returns:

  • A Kleisli arrow that takes a resource-using function and returns a ReaderResult[B] with automatic Close() cleanup

Example - File operations:

import (
    "context"
    "os"
    "io"
)

// Create a reusable file manager
withFile := readerresult.WithCloser(
    func() readerresult.ReaderResult[*os.File] {
        return func(ctx context.Context) (*os.File, error) {
            return os.Open("data.txt")
        }
    },
)

// Use with different operations - Close() is automatic
readContent := withFile(func(file *os.File) readerresult.ReaderResult[string] {
    return func(ctx context.Context) (string, error) {
        data, err := io.ReadAll(file)
        return string(data), err
    }
})

getSize := withFile(func(file *os.File) readerresult.ReaderResult[int64] {
    return func(ctx context.Context) (int64, error) {
        info, err := file.Stat()
        if err != nil {
            return 0, err
        }
        return info.Size(), nil
    }
})

content, err := readContent(context.Background())
size, err := getSize(context.Background())

Example - HTTP response body:

import "net/http"

withResponse := readerresult.WithCloser(
    func() readerresult.ReaderResult[*http.Response] {
        return func(ctx context.Context) (*http.Response, error) {
            return http.Get("https://api.example.com/data")
        }
    },
)

// Body is automatically closed after use
parseJSON := withResponse(func(resp *http.Response) readerresult.ReaderResult[Data] {
    return func(ctx context.Context) (Data, error) {
        var data Data
        err := json.NewDecoder(resp.Body).Decode(&data)
        return data, err
    }
})

Example - Multiple file operations:

// Read from one file, write to another
copyFile := func(src, dst string) readerresult.ReaderResult[int64] {
    withSrc := readerresult.WithCloser(
        func() readerresult.ReaderResult[*os.File] {
            return func(ctx context.Context) (*os.File, error) {
                return os.Open(src)
            }
        },
    )

    withDst := readerresult.WithCloser(
        func() readerresult.ReaderResult[*os.File] {
            return func(ctx context.Context) (*os.File, error) {
                return os.Create(dst)
            }
        },
    )

    return withSrc(func(srcFile *os.File) readerresult.ReaderResult[int64] {
        return withDst(func(dstFile *os.File) readerresult.ReaderResult[int64] {
            return func(ctx context.Context) (int64, error) {
                return io.Copy(dstFile, srcFile)
            }
        })
    })
}

Example - Network connection:

import "net"

withConn := readerresult.WithCloser(
    func() readerresult.ReaderResult[net.Conn] {
        return func(ctx context.Context) (net.Conn, error) {
            return net.Dial("tcp", "localhost:8080")
        }
    },
)

sendData := withConn(func(conn net.Conn) readerresult.ReaderResult[int] {
    return func(ctx context.Context) (int, error) {
        return conn.Write([]byte("Hello, World!"))
    }
})

Note: WithCloser is a convenience wrapper around WithResource that automatically provides the Close() cleanup function. For resources that don't implement io.Closer or require custom cleanup logic, use WithResource or Bracket instead.

func WithContextK

func WithContextK[A, B any](f Kleisli[A, B]) Kleisli[A, B]

WithContextK wraps a Kleisli arrow (a function that returns a ReaderResult) with context cancellation checking.

This is the Kleisli arrow version of WithContext. It takes a function A -> ReaderResult[B] and returns a new function that performs the same transformation but with an added context cancellation check before executing the resulting ReaderResult.

A Kleisli arrow is a function that takes a value and returns a monadic computation. In this case, Kleisli[A, B] = func(A) ReaderResult[B], which represents a function from A to a context-dependent computation that may fail.

WithContextK is particularly useful when composing operations with Chain/Bind, as it ensures that each step in the composition checks for cancellation before proceeding.

Parameters:

  • f: A Kleisli arrow (function from A to ReaderResult[B])

Returns:

  • A new Kleisli arrow that wraps the result of f with context cancellation checking

Example:

// A function that fetches user details
fetchUserDetails := func(userID int) readerresult.ReaderResult[UserDetails] {
    return func(ctx context.Context) (UserDetails, error) {
        // Fetch from database...
        return details, nil
    }
}

// Wrap it to ensure cancellation is checked before each execution
safeFetchDetails := readerresult.WithContextK(fetchUserDetails)

// Use in a composition chain
pipeline := F.Pipe2(
    getUser(42),
    readerresult.Chain(safeFetchDetails), // Checks cancellation before fetching details
)

// If context is cancelled between getUser and fetchUserDetails,
// the details fetch will not execute
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
result, err := pipeline(ctx)

Use Cases:

  • Adding cancellation checks to composed operations
  • Ensuring long-running pipelines respect context cancellation
  • Wrapping third-party functions that don't check context themselves
  • Creating fail-fast behavior in complex operation chains

func WithResource

func WithResource[B, A, ANY any](
	onCreate Lazy[ReaderResult[A]],
	onRelease Kleisli[A, ANY],
) Kleisli[Kleisli[A, B], B]

WithResource creates a higher-order function for resource management with automatic cleanup.

This function provides a more composable alternative to Bracket by creating a function that takes a resource-using function and automatically handles resource acquisition and release. This is particularly useful when you want to reuse the same resource management pattern with different operations.

The pattern is:

  1. Create a resource manager with onCreate and onRelease
  2. Apply it to different use functions as needed
  3. Each application ensures proper resource cleanup

This is useful for:

  • Creating reusable resource management patterns
  • Building resource pools or factories
  • Composing resource-dependent operations
  • Abstracting resource lifecycle management

Type Parameters:

  • B: The type of the result produced by using the resource
  • A: The type of the acquired resource
  • ANY: The type returned by the release action (typically ignored)

Parameters:

  • onCreate: Lazy computation that creates/acquires the resource
  • onRelease: Function that releases the resource (receives the resource)

Returns:

  • A Kleisli arrow that takes a resource-using function and returns a ReaderResult[B] with automatic resource management

Example - Reusable database connection manager:

import (
    "context"
    "database/sql"
)

// Create a reusable DB connection manager
withDB := readerresult.WithResource(
    // onCreate: Acquire connection
    func() readerresult.ReaderResult[*sql.DB] {
        return func(ctx context.Context) (*sql.DB, error) {
            return sql.Open("postgres", connString)
        }
    },
    // onRelease: Close connection
    func(db *sql.DB) readerresult.ReaderResult[any] {
        return func(ctx context.Context) (any, error) {
            return nil, db.Close()
        }
    },
)

// Use the manager with different operations
getUsers := withDB(func(db *sql.DB) readerresult.ReaderResult[[]User] {
    return func(ctx context.Context) ([]User, error) {
        return queryUsers(ctx, db)
    }
})

getOrders := withDB(func(db *sql.DB) readerresult.ReaderResult[[]Order] {
    return func(ctx context.Context) ([]Order, error) {
        return queryOrders(ctx, db)
    }
})

// Both operations automatically manage the connection
users, err := getUsers(context.Background())
orders, err := getOrders(context.Background())

Example - File operations manager:

withFile := readerresult.WithResource(
    func() readerresult.ReaderResult[*os.File] {
        return func(ctx context.Context) (*os.File, error) {
            return os.Open("config.json")
        }
    },
    func(file *os.File) readerresult.ReaderResult[any] {
        return func(ctx context.Context) (any, error) {
            return nil, file.Close()
        }
    },
)

// Different operations on the same file
readConfig := withFile(func(file *os.File) readerresult.ReaderResult[Config] {
    return func(ctx context.Context) (Config, error) {
        return parseConfig(file)
    }
})

validateConfig := withFile(func(file *os.File) readerresult.ReaderResult[bool] {
    return func(ctx context.Context) (bool, error) {
        return validateConfigFile(file)
    }
})

Example - Composing with other operations:

import F "github.com/IBM/fp-go/v2/function"

// Create a pipeline with automatic resource management
processData := F.Pipe2(
    loadData,
    withDB(func(db *sql.DB) readerresult.ReaderResult[Result] {
        return saveToDatabase(db)
    }),
    readerresult.Map(formatResult),
)

type Lazy

type Lazy[A any] = lazy.Lazy[A]

Lazy represents a deferred computation that produces a value of type A when evaluated.

type Lens

type Lens[S, A any] = lens.Lens[S, A]

Lens represents an optic that focuses on a field of type A within a structure of type S.

type Monoid

type Monoid[A any] = monoid.Monoid[ReaderResult[A]]

Monoid represents a monoid structure for ReaderResult values.

func AltMonoid

func AltMonoid[A any](zero Lazy[ReaderResult[A]]) Monoid[A]

AltMonoid creates a Monoid for ReaderResult using Alt semantics with a custom zero.

The Alt semantics means that the monoid operation tries the first computation, and if it fails, tries the second one. The provided zero is used as the empty element.

Type Parameters:

  • A: The value type

Parameters:

  • zero: A lazy ReaderResult[A] to use as the empty element

Returns:

  • A Monoid[ReaderResult[A]] with Alt semantics

Example:

zero := func() readerresult.ReaderResult[int] {
    return readerresult.Left[int](errors.New("empty"))
}
rrMonoid := readerresult.AltMonoid(zero)

rr1 := readerresult.Left[int](errors.New("failed"))
rr2 := readerresult.Right(42)
combined := rrMonoid.Concat(rr1, rr2)
value, err := combined(ctx)  // Returns (42, nil) - uses second on first failure

func AlternativeMonoid

func AlternativeMonoid[A any](m M.Monoid[A]) Monoid[A]

AlternativeMonoid creates a Monoid for ReaderResult using the Alternative semantics.

The Alternative semantics means that the monoid operation tries the first computation, and if it fails, tries the second one. The empty element is a computation that always fails. The inner values are combined using the provided monoid when both computations succeed.

Type Parameters:

  • A: The value type

Parameters:

  • m: A Monoid[A] for combining successful values

Returns:

  • A Monoid[ReaderResult[A]] with Alternative semantics

Example:

import "github.com/IBM/fp-go/v2/monoid"

// Monoid for integers with addition
intMonoid := monoid.MonoidSum[int]()
rrMonoid := readerresult.AlternativeMonoid(intMonoid)

rr1 := readerresult.Right(10)
rr2 := readerresult.Right(20)
combined := rrMonoid.Concat(rr1, rr2)
value, err := combined(ctx)  // Returns (30, nil)

func ApplicativeMonoid

func ApplicativeMonoid[A any](m M.Monoid[A]) Monoid[A]

ApplicativeMonoid creates a Monoid for ReaderResult using Applicative semantics.

The Applicative semantics means that both computations are executed independently, and their results are combined using the provided monoid. If either fails, the entire operation fails.

Type Parameters:

  • A: The value type

Parameters:

  • m: A Monoid[A] for combining successful values

Returns:

  • A Monoid[ReaderResult[A]] with Applicative semantics

Example:

import "github.com/IBM/fp-go/v2/monoid"

// Monoid for integers with addition
intMonoid := monoid.MonoidSum[int]()
rrMonoid := readerresult.ApplicativeMonoid(intMonoid)

rr1 := readerresult.Right(10)
rr2 := readerresult.Right(20)
combined := rrMonoid.Concat(rr1, rr2)
value, err := combined(ctx)  // Returns (30, nil)

type Operator

type Operator[A, B any] = Kleisli[ReaderResult[A], B]

Operator represents a Kleisli arrow that operates on ReaderResult values. It transforms a ReaderResult[A] into a ReaderResult[B], useful for composing context-aware operations.

func Alt

func Alt[A any](second Lazy[ReaderResult[A]]) Operator[A, A]

func Ap

func Ap[B, A any](fa ReaderResult[A]) Operator[func(A) B, B]

Ap is the curried version of MonadAp, useful for function composition.

It fixes the value argument and returns an Operator that can be applied to a ReaderResult containing a function. This is particularly useful in pipelines where you want to apply a fixed value to various functions.

Type Parameters:

  • B: The result type after applying the function
  • A: The input type to the function

Parameters:

  • fa: A ReaderResult containing a value of type A

Returns:

  • An Operator that applies the value to a function wrapped in ReaderResult

Example:

import F "github.com/IBM/fp-go/v2/function"

value := readerresult.Right(32)
addTen := readerresult.Right(N.Add(10))

result := F.Pipe1(
    addTen,
    readerresult.Ap[int](value),
)
output, err := result(ctx)  // Returns (42, nil)

func ApEitherS

func ApEitherS[
	S1, S2, T any](
	setter func(T) func(S1) S2,
	fa Result[T],
) Operator[S1, S2]

ApEitherS applies a Result (Either) in applicative style, combining it with the current state.

IMPORTANT: Result[T] is PURE (side-effect free) - it doesn't depend on context. Use this for pure error-handling values.

Example

ExampleApEitherS demonstrates applying a Result (Either) in applicative style, combining it with the current state in a do-notation chain.

// A Result value
scoreResult := RES.Of(95)

result := F.Pipe1(
	Do(ScoreState{}),
	ApEitherS(
		func(score int) Endomorphism[ScoreState] {
			return func(s ScoreState) ScoreState {
				s.Score = score
				return s
			}
		},
		scoreResult,
	),
)

state, err := result(context.Background())
fmt.Printf("Score: %d, Error: %v\n", state.Score, err)
Output:

Score: 95, Error: <nil>

func ApReaderS

func ApReaderS[
	S1, S2, T any](
	setter func(T) func(S1) S2,
	fa Reader[context.Context, T],
) Operator[S1, S2]

ApReaderS applies a Reader computation in applicative style, combining it with the current state.

IMPORTANT: Reader[Context, T] is EFFECTFUL because it depends on context.Context. Use this for context-dependent operations that cannot fail.

Example

ExampleApReaderS demonstrates applying a Reader computation in applicative style, combining it with the current state in a do-notation chain.

// A Reader that gets environment from context
getEnv := func(ctx context.Context) string {
	if val := ctx.Value("env"); val != nil {
		return val.(string)
	}
	return "development"
}

result := F.Pipe1(
	Do(EnvState{}),
	ApReaderS(
		func(env string) Endomorphism[EnvState] {
			return func(s EnvState) EnvState {
				s.Environment = env
				return s
			}
		},
		getEnv,
	),
)

ctx := context.WithValue(context.Background(), "env", "staging")
state, err := result(ctx)
fmt.Printf("Environment: %s, Error: %v\n", state.Environment, err)
Output:

Environment: staging, Error: <nil>

func ApS

func ApS[S1, S2, T any](
	setter func(T) func(S1) S2,
	fa ReaderResult[T],
) Operator[S1, S2]

ApS attaches a value to a context using applicative style.

IMPORTANT: ApS is for EFFECTFUL FUNCTIONS that depend on context.Context. The ReaderResult parameter is effectful because it depends on context.Context.

Unlike Bind (which sequences operations), ApS can be used when operations are independent and can conceptually run in parallel.

Type Parameters:

  • S1: The input state type
  • S2: The output state type
  • T: The type of value produced by the computation

Parameters:

  • setter: A function that takes the computation result and returns a state updater
  • fa: An effectful ReaderResult computation

Returns:

  • An Operator that transforms ReaderResult[S1] to ReaderResult[S2]

func ApSL

func ApSL[S, T any](
	lens Lens[S, T],
	fa ReaderResult[T],
) Operator[S, S]

ApSL is a variant of ApS that uses a lens to focus on a specific field in the state.

IMPORTANT: ApSL is for EFFECTFUL FUNCTIONS that depend on context.Context. The ReaderResult parameter is effectful because it depends on context.Context.

Instead of providing a setter function, you provide a lens that knows how to get and set the field. This is more convenient when working with nested structures.

Type Parameters:

  • S: The state type
  • T: The type of the field to update

Parameters:

  • lens: A lens that focuses on a field of type T within state S
  • fa: An effectful ReaderResult computation that produces a value of type T

Returns:

  • An Operator that transforms ReaderResult[S] to ReaderResult[S]

func BiMap

func BiMap[A, B any](f Endomorphism[error], g func(A) B) Operator[A, B]

func Bind

func Bind[S1, S2, T any](
	setter func(T) func(S1) S2,
	f Kleisli[S1, T],
) Operator[S1, S2]

Bind sequences an EFFECTFUL ReaderResult computation and updates the state with its result.

IMPORTANT: Bind is for EFFECTFUL FUNCTIONS that depend on context.Context. The Kleisli parameter (State -> ReaderResult[T]) is effectful because ReaderResult depends on context.Context (can be cancelled, has deadlines, carries values).

For PURE FUNCTIONS (side-effect free), use:

  • BindResultK: For pure functions with errors (State -> (Value, error))
  • Let: For pure functions without errors (State -> Value)

This is the core operation for do-notation, allowing you to chain computations where each step can depend on the accumulated state and update it with new values.

Type Parameters:

  • S1: The input state type
  • S2: The output state type
  • T: The type of value produced by the computation

Parameters:

  • setter: A function that takes the computation result and returns a state updater
  • f: A Kleisli arrow that produces the next effectful computation based on current state

Returns:

  • An Operator that transforms ReaderResult[S1] to ReaderResult[S2]
Example

ExampleBind demonstrates sequencing a ReaderResult computation and updating the state with its result. This is the core operation for do-notation, allowing you to chain computations where each step can depend on the accumulated state and update it with new values.

Step-by-step breakdown:

1. Setup lenses for accessing nested state fields:

  • userLenses: Provides lenses for User fields (ID, Name)

  • stateLenses: Provides lenses for State fields (User, Posts, FullName, Status)

  • userIdLens: A composed lens that focuses on state.User.ID Created by composing stateLenses.User with userLenses.ID

    2. Do(State{}) - Initialize the do-notation chain with an empty State. This creates the initial ReaderResult that will accumulate data through subsequent operations.

    3. ApSL(stateLenses.User, getUser(42)) - Fetch user and store in state.User field. ApSL (Applicative Set Lens) executes the getUser(42) ReaderResult computation and uses the lens to set the result into state.User. After this step: state.User = User{ID: 42, Name: "Alice"}

    4. Bind(stateLenses.Posts.Set, F.Flow2(userIdLens.Get, getPosts)) - Fetch posts based on the user ID from state and store them in state.Posts.

    Breaking down the Bind operation: a) First parameter: stateLenses.Posts.Set - A setter function that will update the Posts field in the state with the result of the computation.

    b) Second parameter: F.Flow2(userIdLens.Get, getPosts) - A composed function that:

  • Takes the current state as input

  • Extracts the user ID using userIdLens.Get (gets state.User.ID)

  • Passes the user ID to getPosts, which returns a ReaderResult[[]Post]

  • The result is then set into state.Posts using the setter

    After this step: state.Posts = [{ID: 1, UserID: 42, ...}, {ID: 2, UserID: 42, ...}]

    5. result(context.Background()) - Execute the entire computation chain. This runs all the ReaderResult operations in sequence, threading the context through each step and accumulating the state.

Key concepts demonstrated: - Lens composition: Building complex accessors from simple ones - Sequential effects: Each step can depend on previous results - State accumulation: Building up a complex state object step by step - Context threading: The context.Context flows through all operations - Error handling: Any error in the chain short-circuits execution

userLenses := MakeUserLenses()
stateLenses := MakeStateLenses()

userIdLens := F.Pipe1(
	stateLenses.User,
	lens.Compose[State](userLenses.ID),
)

result := F.Pipe2(
	Do(State{}),
	ApSL(
		stateLenses.User,
		getUser(42),
	),
	Bind(
		stateLenses.Posts.Set,
		F.Flow2(
			userIdLens.Get,
			getPosts,
		),
	),
)

state, err := result(context.Background())
fmt.Printf("User: %s, Posts: %d, Error: %v\n", state.User.Name, len(state.Posts), err)
Output:

User: Alice, Posts: 2, Error: <nil>

func BindEitherK

func BindEitherK[S1, S2, T any](
	setter func(T) func(S1) S2,
	f RES.Kleisli[S1, T],
) Operator[S1, S2]

BindEitherK binds a Result (Either) computation into the do-notation chain.

IMPORTANT: This is for PURE FUNCTIONS (side-effect free) that return Result[T]. The function (State -> Result[T]) is pure - it only depends on state, not context. Use this for pure error-handling logic that doesn't need context.

Example

ExampleBindEitherK demonstrates binding a Result (Either) computation into a ReaderResult do-notation chain. This is useful for integrating pure error-handling logic that doesn't need context.

numberStateLenses := MakeNumberStateLenses()

// A pure function that returns a Result
parseNumber := func(s NumberState) RES.Result[int] {
	return RES.Of(42)
}

result := F.Pipe1(
	Do(NumberState{}),
	BindEitherK(
		numberStateLenses.Number.Set,
		parseNumber,
	),
)

state, err := result(context.Background())
fmt.Printf("Number: %d, Error: %v\n", state.Number, err)
Output:

Number: 42, Error: <nil>

func BindL

func BindL[S, T any](
	lens Lens[S, T],
	f Kleisli[T, T],
) Operator[S, S]

BindL is a variant of Bind that uses a lens to focus on a specific field in the state.

IMPORTANT: BindL is for EFFECTFUL FUNCTIONS that depend on context.Context. The Kleisli parameter returns a ReaderResult, which is effectful.

It combines lens-based field access with monadic composition, allowing you to: 1. Extract a field value using the lens 2. Use that value in an effectful computation that may fail 3. Update the field with the result

Type Parameters:

  • S: The state type
  • T: The type of the field to update

Parameters:

  • lens: A lens that focuses on a field of type T within state S
  • f: An effectful Kleisli arrow that transforms the field value

Returns:

  • An Operator that transforms ReaderResult[S] to ReaderResult[S]

func BindReaderK

func BindReaderK[S1, S2, T any](
	setter func(T) func(S1) S2,
	f reader.Kleisli[context.Context, S1, T],
) Operator[S1, S2]

BindReaderK binds a Reader computation (context-dependent but error-free) into the do-notation chain.

IMPORTANT: This is for functions that depend on context.Context but don't return errors. The Reader[Context, T] is effectful because it depends on context.Context. Use this when you need context values but the operation cannot fail.

Example

ExampleBindReaderK demonstrates binding a Reader computation (context-dependent but error-free) into a ReaderResult do-notation chain.

configStateLenses := MakeConfigStateLenses()

// A Reader that extracts a value from context
getConfig := func(ctx context.Context) string {
	if val := ctx.Value("config"); val != nil {
		return val.(string)
	}
	return "default"
}

result := F.Pipe1(
	Do(ConfigState{}),
	BindReaderK(configStateLenses.Config.Set,
		func(s ConfigState) Reader[context.Context, string] {
			return getConfig
		},
	),
)

ctx := context.WithValue(context.Background(), "config", "production")
state, err := result(ctx)
fmt.Printf("Config: %s, Error: %v\n", state.Config, err)
Output:

Config: production, Error: <nil>

func BindResultK

func BindResultK[S1, S2, T any](
	setter func(T) func(S1) S2,
	f result.Kleisli[S1, T],
) Operator[S1, S2]

BindResultK binds an idiomatic Go function (returning value and error) into the do-notation chain.

IMPORTANT: This is for PURE FUNCTIONS (side-effect free) that return (Value, error). The function (State -> (Value, error)) is pure - it only depends on state, not context. Use this for pure computations with error handling that don't need context.

For EFFECTFUL FUNCTIONS (that need context.Context), use Bind instead.

Example

ExampleBindResultK demonstrates binding an idiomatic Go function (returning value and error) into a ReaderResult do-notation chain. This is particularly useful for integrating existing Go code that follows the standard (value, error) return pattern into functional pipelines.

Step-by-step breakdown:

  1. dataStateLenses := MakeDataStateLenses() - Create lenses for accessing DataState fields. This provides functional accessors (getters and setters) for the Data field, enabling type-safe, immutable field updates.

  2. fetchData := func(s DataState) (string, error) - Define an idiomatic Go function that takes the current state and returns a tuple of (value, error).

    IMPORTANT: This function represents a PURE READER COMPOSITION - it reads from the state and performs computations that don't require a context.Context. This is suitable for: - Pure computations that may fail (parsing, validation, calculations) - Operations that only depend on the state, not external context - Stateless transformations with error handling - Synchronous operations that don't need cancellation or timeouts

    For EFFECTFUL COMPOSITION (operations that need context), use the full ReaderResult type instead: func(context.Context) (Value, error) Use ReaderResult when you need: - Context cancellation or timeouts - Context values (request IDs, trace IDs, etc.) - Operations that depend on external context state - Async operations that should respect context lifecycle

    In this example, fetchData always succeeds with "fetched data", but in real code it might perform pure operations like: - Parsing or validating data from the state - Performing calculations that could fail - Calling pure functions from external libraries - Data transformations that don't require context

  3. Do(DataState{}) - Initialize the do-notation chain with an empty DataState. This creates the initial ReaderResult that will accumulate data through subsequent operations. Initial state: {Data: ""}

  4. BindResultK(dataStateLenses.Data.Set, fetchData) - Bind the idiomatic Go function into the ReaderResult chain.

    BindResultK takes two parameters:

    a) First parameter: dataStateLenses.Data.Set This is a setter function from the lens that will update the Data field with the result of the computation. The lens ensures immutable updates.

    b) Second parameter: fetchData This is the idiomatic Go function (State -> (Value, error)) that will be lifted into the ReaderResult context.

    The BindResultK operation flow: - Takes the current state: {Data: ""} - Calls fetchData with the state: fetchData(DataState{}) - Gets the result tuple: ("fetched data", nil) - If error is not nil, short-circuits the chain and returns the error - If error is nil, uses the setter to update state.Data with "fetched data" - Returns the updated state: {Data: "fetched data"} After this step: {Data: "fetched data"}

  5. result(context.Background()) - Execute the computation chain with a context. Even though fetchData doesn't use the context, the ReaderResult still needs one to maintain the uniform interface. This runs all operations in sequence and returns the final state and any error.

Key concepts demonstrated: - Integration of idiomatic Go code: BindResultK bridges functional and imperative styles - Error propagation: Errors from the Go function automatically propagate through the chain - State transformation: The result updates the state using lens-based setters - Context independence: The function doesn't need context but still works in ReaderResult

Comparison with other bind operations: - BindResultK: For idiomatic Go functions (State -> (Value, error)) - Bind: For full ReaderResult computations (State -> ReaderResult[Value]) - BindEitherK: For pure Result/Either values (State -> Result[Value]) - BindReaderK: For context-dependent functions (State -> Reader[Context, Value])

Use BindResultK when you need to: - Integrate existing Go code that returns (value, error) - Call functions that may fail but don't need context - Perform stateful computations with standard Go error handling - Bridge between functional pipelines and imperative Go code - Work with libraries that follow Go conventions

Real-world example scenarios: - Parsing JSON from a state field: func(s State) (ParsedData, error) - Validating user input: func(s State) (ValidatedInput, error) - Performing calculations: func(s State) (Result, error) - Calling third-party libraries: func(s State) (APIResponse, error)

dataStateLenses := MakeDataStateLenses()

// An idiomatic Go function returning (value, error)
fetchData := func(s DataState) (string, error) {
	return "fetched data", nil
}

result := F.Pipe1(
	Do(DataState{}),
	BindResultK(
		dataStateLenses.Data.Set,
		fetchData,
	),
)

state, err := result(context.Background())
fmt.Printf("Data: %s, Error: %v\n", state.Data, err)
Output:

Data: fetched data, Error: <nil>

func BindTo

func BindTo[S1, T any](
	setter func(T) S1,
) Operator[T, S1]

BindTo initializes do-notation by binding a value to a state.

This is typically used as the first operation after a computation to start building up a state structure.

Type Parameters:

  • S1: The state type to create
  • T: The type of the initial value

Parameters:

  • setter: A function that creates the initial state from a value

Returns:

  • An Operator that transforms ReaderResult[T] to ReaderResult[S1]
Example

ExampleBindTo demonstrates initializing do-notation by binding a value to a state. This is typically used as the first operation after a computation to start building up a state structure.

userStatePrisms := MakeUserStatePrisms()

result := F.Pipe1(
	getUser(42),
	BindToP(userStatePrisms.User),
)

state, err := result(context.Background())
fmt.Printf("User: %s, Error: %v\n", state.User.Name, err)
Output:

User: Alice, Error: <nil>

func BindToP

func BindToP[S1, T any](
	setter Prism[S1, T],
) Operator[T, S1]

BindToP initializes do-notation by binding a value to a state using a Prism.

This is a variant of BindTo that uses a prism instead of a setter function. Prisms are useful for working with sum types and optional values.

Type Parameters:

  • S1: The state type to create
  • T: The type of the initial value

Parameters:

  • setter: A prism that can construct the state from a value

Returns:

  • An Operator that transforms ReaderResult[T] to ReaderResult[S1]

func Chain

func Chain[A, B any](f Kleisli[A, B]) Operator[A, B]

Chain is the curried version of MonadChain, useful for function composition.

It returns an Operator that can be used in pipelines with F.Pipe.

Type Parameters:

  • A: The input value type
  • B: The output value type

Parameters:

  • f: A Kleisli arrow for the second computation

Returns:

  • An Operator that chains ReaderResult computations

func ChainEitherK

func ChainEitherK[A, B any](f RES.Kleisli[A, B]) Operator[A, B]

func ChainReaderK

func ChainReaderK[A, B any](f result.Kleisli[A, B]) Operator[A, B]

func Flap

func Flap[B, A any](a A) Operator[func(A) B, B]

func Let

func Let[S1, S2, T any](
	setter func(T) func(S1) S2,
	f func(S1) T,
) Operator[S1, S2]

Let attaches the result of a PURE computation to a state.

IMPORTANT: Let is for PURE FUNCTIONS (side-effect free) that don't depend on context.Context. The function parameter (State -> Value) is pure - it only reads from state with no effects.

For EFFECTFUL FUNCTIONS (that need context.Context), use:

  • Bind: For effectful ReaderResult computations (State -> ReaderResult[Value])

For PURE FUNCTIONS with error handling, use:

  • BindResultK: For pure functions with errors (State -> (Value, error))

Unlike Bind, Let works with pure functions (not ReaderResult computations). This is useful for deriving values from the current state without performing any effects.

Type Parameters:

  • S1: The input state type
  • S2: The output state type
  • T: The type of value computed

Parameters:

  • setter: A function that takes the computed value and returns a state updater
  • f: A pure function that computes a value from the current state

Returns:

  • An Operator that transforms ReaderResult[S1] to ReaderResult[S2]
Example

ExampleLet demonstrates attaching the result of a pure computation to a state. Unlike Bind, Let works with pure functions (not ReaderResult computations). This is useful for deriving values from the current state without performing any effects.

Step-by-step breakdown:

  1. nameStateLenses := MakeNameStateLenses() - Create lenses for accessing NameState fields. Lenses provide a functional way to get and set nested fields in immutable data structures. This gives us lenses for FirstName, LastName, and FullName fields.

  2. Do(NameState{FirstName: "John", LastName: "Doe"}) - Initialize the do-notation chain with a NameState containing first and last names. Initial state: {FirstName: "John", LastName: "Doe", FullName: ""}

  3. Let(nameStateLenses.FullName.Set, func(s NameState) string {...}) - Compute a derived value from the current state and update the state with it.

    Let takes two parameters:

    a) First parameter: nameStateLenses.FullName.Set This is a setter function (from the lens) that takes a value and returns a function to update the FullName field in the state. The lens-based setter ensures immutable updates.

    b) Second parameter: func(s NameState) string This is a pure "getter" or "computation" function that derives a value from the current state. Here it concatenates FirstName and LastName with a space. This function has no side effects - it just computes a value.

    The Let operation flow: - Takes the current state: {FirstName: "John", LastName: "Doe", FullName: ""} - Calls the computation function: "John" + " " + "Doe" = "John Doe" - Passes "John Doe" to the setter (nameStateLenses.FullName.Set) - The setter creates a new state with FullName updated After this step: {FirstName: "John", LastName: "Doe", FullName: "John Doe"}

  4. Map(nameStateLenses.FullName.Get) - Transform the final state to extract just the FullName field using the lens getter. This changes the result type from ReaderResult[NameState] to ReaderResult[string].

  5. result(context.Background()) - Execute the computation chain and return the final extracted value ("John Doe") and any error.

Key differences between Let and Bind: - Let: Works with pure functions (State -> Value), no effects or errors - Bind: Works with effectful computations (State -> ReaderResult[Value]) - Let: Used for deriving/computing values from existing state - Bind: Used for operations that may fail, need context, or have side effects

Use Let when you need to: - Compute derived values from existing state fields - Transform or combine state values without side effects - Add computed fields to your state for later use in the pipeline - Perform pure calculations that don't require context or error handling

nameStateLenses := MakeNameStateLenses()

result := F.Pipe2(
	Do(NameState{FirstName: "John", LastName: "Doe"}),
	Let(nameStateLenses.FullName.Set,
		func(s NameState) string {
			return s.FirstName + " " + s.LastName
		},
	),
	Map(nameStateLenses.FullName.Get),
)

fullName, err := result(context.Background())
fmt.Printf("Full Name: %s, Error: %v\n", fullName, err)
Output:

Full Name: John Doe, Error: <nil>

func LetL

func LetL[S, T any](
	lens Lens[S, T],
	f Endomorphism[T],
) Operator[S, S]

LetL is a variant of Let that uses a lens to focus on a specific field in the state.

IMPORTANT: LetL is for PURE FUNCTIONS (side-effect free) that don't depend on context.Context. The endomorphism parameter is a pure function (T -> T) with no errors or effects.

It applies a pure transformation to the focused field without any effects.

Type Parameters:

  • S: The state type
  • T: The type of the field to update

Parameters:

  • lens: A lens that focuses on a field of type T within state S
  • f: A pure endomorphism that transforms the field value

Returns:

  • An Operator that transforms ReaderResult[S] to ReaderResult[S]

func LetTo

func LetTo[S1, S2, T any](
	setter func(T) func(S1) S2,
	b T,
) Operator[S1, S2]

LetTo attaches a constant value to a state. This is a PURE operation (side-effect free).

This is a simplified version of Let for when you want to add a constant value to the state without computing it.

Type Parameters:

  • S1: The input state type
  • S2: The output state type
  • T: The type of the constant value

Parameters:

  • setter: A function that takes the constant and returns a state updater
  • b: The constant value to attach

Returns:

  • An Operator that transforms ReaderResult[S1] to ReaderResult[S2]
Example

ExampleLetTo demonstrates attaching a constant value to a state. This is a simplified version of Let for when you want to add a constant value to the state without computing it.

Step-by-step breakdown:

  1. statusStateLenses := MakeStatusStateLenses() - Create lenses for accessing StatusState fields. This provides functional accessors (getters and setters) for the Status field.

  2. Do(StatusState{}) - Initialize the do-notation chain with an empty StatusState. Initial state: {Status: ""}

  3. LetToL(statusStateLenses.Status, "active") - Set the Status field to the constant value "active".

    LetToL is the lens-based version of LetTo and takes two parameters:

    a) First parameter: statusStateLenses.Status This is a lens that focuses on the Status field. The lens provides both a getter and setter for the field, enabling immutable updates.

    b) Second parameter: "active" This is the constant value to assign to the Status field. Unlike Let, which takes a function to compute the value, LetToL directly takes the value itself.

    The LetToL operation: - Takes the constant value "active" - Uses the lens setter to create a new state with Status = "active" - Returns the updated state After this step: {Status: "active"}

  4. Map(statusStateLenses.Status.Get) - Transform the final state to extract just the Status field using the lens getter. This changes the result type from ReaderResult[StatusState] to ReaderResult[string].

  5. result(context.Background()) - Execute the computation chain and return the final extracted value ("active") and any error.

Comparison of state-setting operations: - LetToL: Set a field to a constant value using a lens (simplest) - LetL: Transform a field using a function and a lens - Let: Compute a value from state and update using a custom setter - Bind: Execute an effectful computation and update state with the result

Use LetToL when you need to: - Set a field to a known constant value - Initialize state fields with default values - Update configuration or status flags - Assign literal values without any computation

LetToL is the most straightforward way to set a constant value in do-notation, combining the simplicity of LetTo with the power of lenses for type-safe, immutable field updates.

statusStateLenses := MakeStatusStateLenses()

result := F.Pipe2(
	Do(StatusState{}),
	LetToL(
		statusStateLenses.Status,
		"active",
	),
	Map(statusStateLenses.Status.Get),
)

status, err := result(context.Background())
fmt.Printf("Status: %s, Error: %v\n", status, err)
Output:

Status: active, Error: <nil>

func LetToL

func LetToL[S, T any](
	lens Lens[S, T],
	b T,
) Operator[S, S]

LetToL is a variant of LetTo that uses a lens to focus on a specific field in the state.

IMPORTANT: LetToL is for setting constant values. This is a PURE operation (side-effect free).

It sets the focused field to a constant value.

Type Parameters:

  • S: The state type
  • T: The type of the field to update

Parameters:

  • lens: A lens that focuses on a field of type T within state S
  • b: The constant value to set

Returns:

  • An Operator that transforms ReaderResult[S] to ReaderResult[S]

func Local

func Local[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A]

Local transforms the context.Context environment before passing it to a ReaderResult computation.

This is the Reader's local operation, which allows you to modify the environment for a specific computation without affecting the outer context. The transformation function receives the current context and returns a new context along with a cancel function. The cancel function is automatically called when the computation completes (via defer), ensuring proper cleanup of resources.

This is useful for:

  • Adding timeouts or deadlines to specific operations
  • Adding context values for nested computations
  • Creating isolated context scopes
  • Implementing context-based dependency injection

Type Parameters:

  • A: The value type of the ReaderResult

Parameters:

  • f: A function that transforms the context and returns a cancel function

Returns:

  • An Operator that runs the computation with the transformed context

Example:

import F "github.com/IBM/fp-go/v2/function"

// Add a custom value to the context
type key int
const userKey key = 0

addUser := readerresult.Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
    newCtx := context.WithValue(ctx, userKey, "Alice")
    return newCtx, func() {} // No-op cancel
})

getUser := readerresult.Asks(func(ctx context.Context) string {
    return ctx.Value(userKey).(string)
})

result := F.Pipe1(
    getUser,
    addUser,
)
user, err := result(context.Background())  // Returns ("Alice", nil)

Timeout Example:

// Add a 5-second timeout to a specific operation
withTimeout := readerresult.Local[Data](func(ctx context.Context) (context.Context, context.CancelFunc) {
    return context.WithTimeout(ctx, 5*time.Second)
})

result := F.Pipe1(
    fetchData,
    withTimeout,
)

func Map

func Map[A, B any](f func(A) B) Operator[A, B]

Map is the curried version of MonadMap, useful for function composition.

It returns an Operator that can be used in pipelines with F.Pipe.

Type Parameters:

  • A: The input value type
  • B: The output value type

Parameters:

  • f: The transformation function

Returns:

  • An Operator that transforms ReaderResult[A] to ReaderResult[B]

func MapLeft

func MapLeft[A any](f Endomorphism[error]) Operator[A, A]

func OrElse

func OrElse[A any](onLeft Kleisli[error, A]) Operator[A, A]

func OrLeft

func OrLeft[A any](onLeft reader.Kleisli[context.Context, error, error]) Operator[A, A]

func WithDeadline

func WithDeadline[A any](deadline time.Time) Operator[A, A]

WithDeadline adds an absolute deadline to the context for a ReaderResult computation.

This is a convenience wrapper around Local that uses context.WithDeadline. The computation must complete before the specified time, or it will be cancelled. This is useful for coordinating operations that must finish by a specific time, such as request deadlines or scheduled tasks.

The deadline is an absolute time, unlike WithTimeout which uses a relative duration. The cancel function is automatically called when the computation completes, ensuring proper cleanup.

Type Parameters:

  • A: The value type of the ReaderResult

Parameters:

  • deadline: The absolute time by which the computation must complete

Returns:

  • An Operator that runs the computation with a deadline

func WithTimeout

func WithTimeout[A any](timeout time.Duration) Operator[A, A]

WithTimeout adds a timeout to the context for a ReaderResult computation.

This is a convenience wrapper around Local that uses context.WithTimeout. The computation must complete within the specified duration, or it will be cancelled. This is useful for ensuring operations don't run indefinitely and for implementing timeout-based error handling.

The timeout is relative to when the ReaderResult is executed, not when WithTimeout is called. The cancel function is automatically called when the computation completes, ensuring proper cleanup.

Type Parameters:

  • A: The value type of the ReaderResult

Parameters:

  • timeout: The maximum duration for the computation

Returns:

  • An Operator that runs the computation with a timeout

Example:

import (
    "time"
    F "github.com/IBM/fp-go/v2/function"
)

// Fetch data with a 5-second timeout
fetchData := readerresult.FromReader(func(ctx context.Context) Data {
    // Simulate slow operation
    select {
    case <-time.After(10 * time.Second):
        return Data{Value: "slow"}
    case <-ctx.Done():
        return Data{}
    }
})

result := F.Pipe1(
    fetchData,
    readerresult.WithTimeout[Data](5*time.Second),
)
_, err := result(context.Background())  // Returns context.DeadlineExceeded after 5s

Successful Example:

quickFetch := readerresult.Right(Data{Value: "quick"})
result := F.Pipe1(
    quickFetch,
    readerresult.WithTimeout[Data](5*time.Second),
)
data, err := result(context.Background())  // Returns (Data{Value: "quick"}, nil)

type Option

type Option[A any] = option.Option[A]

Option represents an optional value that may or may not be present.

type Prism

type Prism[S, A any] = prism.Prism[S, A]

Prism represents an optic that focuses on a case of type A within a sum type S.

type Reader

type Reader[R, A any] = reader.Reader[R, A]

Reader represents a computation that depends on a read-only environment of type R and produces a value of type A.

type ReaderResult

type ReaderResult[A any] = func(context.Context) (A, error)

ReaderResult represents a computation that depends on a context.Context and produces either a value of type A or an error. It combines the Reader pattern with Result (error handling), making it suitable for context-aware operations that may fail.

func Ask

func Ask() ReaderResult[context.Context]

Ask retrieves the current context.Context environment.

This is the Reader's ask operation, which provides access to the environment. It always succeeds and returns the context that was passed in.

Returns:

  • A ReaderResult[context.Context] that returns the environment
Example

ExampleAsk demonstrates getting the context.Context environment. This returns a ReaderResult that provides access to the context itself.

rr := Ask()
ctx := context.Background()
value, err := rr(ctx)
fmt.Println(value == ctx, err)
Output:

true <nil>

func Asks

func Asks[A any](r Reader[context.Context, A]) ReaderResult[A]

Asks extracts a value from the context.Context environment using a Reader function.

This is useful for accessing specific parts of the environment. The Reader function is applied to the context, and the result is wrapped in a successful ReaderResult.

Type Parameters:

  • A: The extracted value type

Parameters:

  • r: A Reader function that extracts a value from the context

Returns:

  • A ReaderResult[A] that extracts and returns the value
Example

ExampleAsks demonstrates extracting a value from the context using a function. This is useful for accessing configuration or other data stored in the context.

type Config struct {
	Port int
}

getPort := Asks(func(ctx context.Context) int {
	// In real code, extract config from context
	return 8080
})

value, err := getPort(context.Background())
fmt.Println(value, err)
Output:

8080 <nil>

func Bracket

func Bracket[
	A, B, ANY any](

	acquire Lazy[ReaderResult[A]],
	use Kleisli[A, B],
	release func(A, B, error) ReaderResult[ANY],
) ReaderResult[B]

Bracket ensures safe resource management with guaranteed cleanup in the ReaderResult monad.

This function implements the bracket pattern (also known as try-with-resources or RAII) for ReaderResult computations. It guarantees that the release action is called regardless of whether the use action succeeds or fails, making it ideal for managing resources like file handles, database connections, network sockets, or locks.

The execution flow is:

  1. Acquire the resource (lazily evaluated)
  2. Use the resource with the provided function
  3. Release the resource with access to: the resource, the result (if successful), and any error

The release function is always called, even if:

  • The acquire action fails (release is not called in this case)
  • The use action fails (release receives the error)
  • The use action succeeds (release receives nil error)

Type Parameters:

  • A: The type of the acquired resource
  • B: The type of the result produced by using the resource
  • ANY: The type returned by the release action (typically ignored)

Parameters:

  • acquire: Lazy computation that acquires the resource
  • use: Function that uses the resource to produce a result
  • release: Function that releases the resource, receiving the resource, result, and any error

Returns:

  • A ReaderResult[B] that safely manages the resource lifecycle

Example - File handling:

import (
    "context"
    "os"
)

readFile := readerresult.Bracket(
    // Acquire: Open file
    func() readerresult.ReaderResult[*os.File] {
        return func(ctx context.Context) (*os.File, error) {
            return os.Open("data.txt")
        }
    },
    // Use: Read file contents
    func(file *os.File) readerresult.ReaderResult[string] {
        return func(ctx context.Context) (string, error) {
            data, err := io.ReadAll(file)
            return string(data), err
        }
    },
    // Release: Close file (always called)
    func(file *os.File, content string, err error) readerresult.ReaderResult[any] {
        return func(ctx context.Context) (any, error) {
            return nil, file.Close()
        }
    },
)

content, err := readFile(context.Background())

Example - Database connection:

queryDB := readerresult.Bracket(
    // Acquire: Open connection
    func() readerresult.ReaderResult[*sql.DB] {
        return func(ctx context.Context) (*sql.DB, error) {
            return sql.Open("postgres", connString)
        }
    },
    // Use: Execute query
    func(db *sql.DB) readerresult.ReaderResult[[]User] {
        return func(ctx context.Context) ([]User, error) {
            return queryUsers(ctx, db)
        }
    },
    // Release: Close connection (always called)
    func(db *sql.DB, users []User, err error) readerresult.ReaderResult[any] {
        return func(ctx context.Context) (any, error) {
            return nil, db.Close()
        }
    },
)

Example - Lock management:

withLock := readerresult.Bracket(
    // Acquire: Lock mutex
    func() readerresult.ReaderResult[*sync.Mutex] {
        return func(ctx context.Context) (*sync.Mutex, error) {
            mu.Lock()
            return mu, nil
        }
    },
    // Use: Perform critical section work
    func(mu *sync.Mutex) readerresult.ReaderResult[int] {
        return func(ctx context.Context) (int, error) {
            return performCriticalWork(ctx)
        }
    },
    // Release: Unlock mutex (always called)
    func(mu *sync.Mutex, result int, err error) readerresult.ReaderResult[any] {
        return func(ctx context.Context) (any, error) {
            mu.Unlock()
            return nil, nil
        }
    },
)

func Curry0

func Curry0[A any](f func(context.Context) (A, error)) ReaderResult[A]

Curry0 converts a function that takes context.Context and returns (A, error) into a ReaderResult[A].

This is useful for lifting existing functions that follow Go's context-first convention into the ReaderResult monad.

Type Parameters:

  • A: The return value type

Parameters:

  • f: A function that takes context.Context and returns (A, error)

Returns:

  • A ReaderResult[A] that wraps the function

Example:

func getConfig(ctx context.Context) (Config, error) {
    // ... implementation
    return config, nil
}
rr := readerresult.Curry0(getConfig)
config, err := rr(ctx)

func Do

func Do[S any](
	empty S,
) ReaderResult[S]

Do initializes a do-notation context with an empty state.

This is the starting point for do-notation style composition, which allows imperative-style sequencing of ReaderResult computations while maintaining functional purity.

Type Parameters:

  • S: The state type

Parameters:

  • empty: The initial empty state

Returns:

  • A ReaderResult[S] containing the initial state
Example

ExampleDo demonstrates initializing a do-notation context with an empty state. This is the starting point for do-notation style composition, which allows imperative-style sequencing of ReaderResult computations while maintaining functional purity.

Step-by-step breakdown:

  1. Do(SimpleState{}) - Initialize the do-notation chain with an empty SimpleState. This creates a ReaderResult that, when executed, will return the initial state. The state acts as an accumulator that will be threaded through subsequent operations.

  2. LetToL(simpleStateLenses.Value, 42) - Set the Value field to the constant 42. LetToL uses a lens to focus on a specific field in the state and assign a constant value. The "L" suffix indicates this is the lens-based version of LetTo. After this step, state.Value = 42.

  3. LetL(simpleStateLenses.Value, N.Add(8)) - Transform the Value field by adding 8. LetL uses a lens to focus on a field and apply a transformation function to it. N.Add(8) creates a function that adds 8 to its input. After this step, state.Value = 42 + 8 = 50.

  4. result(context.Background()) - Execute the composed ReaderResult computation. This runs the entire chain with the provided context and returns the final state and any error that occurred during execution.

The key insight: Do-notation allows you to build complex stateful computations in a declarative, pipeline style while maintaining immutability and composability.

simpleStateLenses := MakeSimpleStateLenses()

result := F.Pipe2(
	Do(SimpleState{}),
	LetToL(
		simpleStateLenses.Value,
		42,
	),
	LetL(
		simpleStateLenses.Value,
		N.Add(8),
	),
)

state, err := result(context.Background())
fmt.Printf("Value: %d, Error: %v\n", state.Value, err)
Output:

Value: 50, Error: <nil>

func Flatten

func Flatten[A any](mma ReaderResult[ReaderResult[A]]) ReaderResult[A]

Flatten removes one level of ReaderResult nesting.

This is equivalent to Chain with the identity function. It's useful when you have a ReaderResult that produces another ReaderResult and want to collapse them into one.

Type Parameters:

  • A: The inner value type

Parameters:

  • mma: A nested ReaderResult[ReaderResult[A]]

Returns:

  • A flattened ReaderResult[A]

func FromEither

func FromEither[A any](e Result[A]) ReaderResult[A]

FromEither lifts a Result (Either[error, A]) into a ReaderResult.

The resulting ReaderResult ignores the context.Context environment and simply returns the Result value. This is useful for converting existing Result values into the ReaderResult monad for composition with other ReaderResult operations.

Type Parameters:

  • A: The success value type

Parameters:

  • e: A Result[A] (Either[error, A]) to lift

Returns:

  • A ReaderResult[A] that ignores the context and returns the Result
Example

ExampleFromEither demonstrates lifting a Result (Either) into a The resulting ReaderResult ignores the context and returns the Result value.

res := RES.Of(42)
rr := FromEither(res)
value, err := rr(context.Background())
fmt.Println(value, err)
Output:

42 <nil>

func FromReader

func FromReader[A any](r Reader[context.Context, A]) ReaderResult[A]

FromReader lifts a Reader into a ReaderResult that always succeeds.

The Reader computation is executed and its result is wrapped in a successful Result. This is useful for incorporating Reader computations into ReaderResult pipelines.

Type Parameters:

  • A: The value type

Parameters:

  • r: A Reader[context.Context, A] to lift

Returns:

  • A ReaderResult[A] that executes the Reader and always succeeds

func FromReaderResult

func FromReaderResult[A any](r RS.ReaderResult[A]) ReaderResult[A]

func FromResult

func FromResult[A any](a A, err error) ReaderResult[A]

FromResult creates a ReaderResult from a Go-style (value, error) tuple.

This is a convenience function for converting standard Go error handling into the ReaderResult monad. The resulting ReaderResult ignores the context.

Type Parameters:

  • A: The value type

Parameters:

  • a: The value
  • err: The error (nil for success)

Returns:

  • A ReaderResult[A] that returns the given value and error
Example

ExampleFromResult demonstrates creating a ReaderResult from a Go-style (value, error) tuple. This is useful for converting standard Go error handling into the ReaderResult monad.

rr := FromResult(42, nil)
value, err := rr(context.Background())
fmt.Println(value, err)
Output:

42 <nil>
Example (Error)

ExampleFromResult_error demonstrates creating a ReaderResult from an error case. The resulting ReaderResult will propagate the error when executed.

rr := FromResult(0, errors.New("failed"))
value, err := rr(context.Background())
fmt.Println(value, err != nil)
Output:

0 true

func Left

func Left[A any](err error) ReaderResult[A]

Left creates a ReaderResult that always fails with the given error.

This is the error constructor for ReaderResult, analogous to Either's Left. The resulting computation ignores the context and immediately returns the error.

Type Parameters:

  • A: The success type (for type inference)

Parameters:

  • err: The error to return

Returns:

  • A ReaderResult[A] that always fails with the given error
Example

ExampleLeft demonstrates creating a ReaderResult that always fails with an error. This is the error constructor for ReaderResult, analogous to Either's Left.

rr := Left[int](errors.New("failed"))
value, err := rr(context.Background())
fmt.Println(value, err != nil)
Output:

0 true

func LeftReader

func LeftReader[A, R any](l Reader[context.Context, error]) ReaderResult[A]

func MonadAlt

func MonadAlt[A any](first ReaderResult[A], second Lazy[ReaderResult[A]]) ReaderResult[A]

func MonadAp

func MonadAp[B, A any](fab ReaderResult[func(A) B], fa ReaderResult[A]) ReaderResult[B]

MonadAp applies a function wrapped in a ReaderResult to a value wrapped in a ReaderResult.

This is the Applicative's ap operation. Both computations are executed concurrently using goroutines, and the context is shared between them. If either computation fails, the entire operation fails. If the context is cancelled, the operation is aborted.

The concurrent execution allows for parallel independent computations, which can improve performance when both operations involve I/O or other blocking operations.

Type Parameters:

  • B: The result type after applying the function
  • A: The input type to the function

Parameters:

  • fab: A ReaderResult containing a function from A to B
  • fa: A ReaderResult containing a value of type A

Returns:

  • A ReaderResult[B] that applies the function to the value

Example:

// Create a function wrapped in ReaderResult
addTen := readerresult.Right(func(n int) int {
    return n + 10
})

// Create a value wrapped in ReaderResult
value := readerresult.Right(32)

// Apply the function to the value
result := readerresult.MonadAp(addTen, value)
output, err := result(ctx)  // Returns (42, nil)

Error Handling:

// If the function fails
failedFn := readerresult.Left[func(int) int](errors.New("function error"))
result := readerresult.MonadAp(failedFn, value)
_, err := result(ctx)  // Returns function error

// If the value fails
failedValue := readerresult.Left[int](errors.New("value error"))
result := readerresult.MonadAp(addTen, failedValue)
_, err := result(ctx)  // Returns value error

Context Cancellation:

ctx, cancel := context.WithCancel(context.Background())
cancel()  // Cancel immediately
result := readerresult.MonadAp(addTen, value)
_, err := result(ctx)  // Returns context cancellation error

func MonadBiMap

func MonadBiMap[A, B any](fa ReaderResult[A], f Endomorphism[error], g func(A) B) ReaderResult[B]

func MonadChain

func MonadChain[A, B any](ma ReaderResult[A], f Kleisli[A, B]) ReaderResult[B]

MonadChain sequences two ReaderResult computations where the second depends on the first.

This is the monadic bind operation (flatMap). If the first computation fails, the error is propagated and the second computation is not executed. Both computations share the same context.Context environment.

Type Parameters:

  • A: The input value type
  • B: The output value type

Parameters:

  • ma: The first ReaderResult computation
  • f: A Kleisli arrow that produces the second computation based on the first's result

Returns:

  • A ReaderResult[B] representing the sequenced computation

func MonadChainEitherK

func MonadChainEitherK[A, B any](ma ReaderResult[A], f RES.Kleisli[A, B]) ReaderResult[B]

func MonadChainReaderK

func MonadChainReaderK[A, B any](ma ReaderResult[A], f result.Kleisli[A, B]) ReaderResult[B]

func MonadFlap

func MonadFlap[A, B any](fab ReaderResult[func(A) B], a A) ReaderResult[B]

func MonadMap

func MonadMap[A, B any](fa ReaderResult[A], f func(A) B) ReaderResult[B]

MonadMap transforms the success value of a ReaderResult using the given function.

If the ReaderResult fails, the error is propagated unchanged. This is the Functor's map operation for ReaderResult.

Type Parameters:

  • A: The input value type
  • B: The output value type

Parameters:

  • fa: The ReaderResult to transform
  • f: The transformation function

Returns:

  • A ReaderResult[B] with the transformed value

func MonadMapLeft

func MonadMapLeft[A any](fa ReaderResult[A], f Endomorphism[error]) ReaderResult[A]

func MonadTraverseArray

func MonadTraverseArray[A, B any](as []A, f Kleisli[A, B]) ReaderResult[[]B]

func Of

func Of[A any](a A) ReaderResult[A]

Of creates a ReaderResult that always succeeds with the given value.

This is an alias for Right and represents the Applicative's pure/return operation. The resulting computation ignores the context and immediately returns the value.

Type Parameters:

  • A: The value type

Parameters:

  • a: The value to wrap

Returns:

  • A ReaderResult[A] that always succeeds with the given value
Example

ExampleOf demonstrates the monadic return/pure operation for It creates a ReaderResult that always succeeds with the given value.

rr := Of(42)
value, err := rr(context.Background())
fmt.Println(value, err)
Output:

42 <nil>

func Retrying

func Retrying[A any](
	policy R.RetryPolicy,
	action Kleisli[R.RetryStatus, A],
	check func(A, error) bool,
) ReaderResult[A]

Retrying retries a ReaderResult computation according to a retry policy with context awareness.

This is the idiomatic wrapper around the functional github.com/IBM/fp-go/v2/context/readerresult.Retrying function. It provides a more Go-friendly API by working with (value, error) tuples instead of Result types.

The function implements a retry mechanism for operations that depend on a context.Context and can fail. It respects context cancellation, meaning that if the context is cancelled during retry delays, the operation will stop immediately and return the cancellation error.

The retry loop will continue until one of the following occurs:

  • The action succeeds and the check function returns false (no retry needed)
  • The retry policy returns None (retry limit reached)
  • The check function returns false (indicating success or a non-retryable failure)
  • The context is cancelled (returns context.Canceled or context.DeadlineExceeded)

Parameters:

  • policy: A RetryPolicy that determines when and how long to wait between retries. The policy receives a RetryStatus on each iteration and returns an optional delay. If it returns None, retrying stops. Common policies include LimitRetries, ExponentialBackoff, and CapDelay from the retry package.

  • action: A Kleisli arrow that takes a RetryStatus and returns a ReaderResult[A]. This function is called on each retry attempt and receives information about the current retry state (iteration number, cumulative delay, etc.). The action depends on a context.Context and produces (A, error). The context passed to the action will be the same context used for retry delays, so cancellation is properly propagated.

  • check: A predicate function that examines the result value and error, returning true if the operation should be retried, or false if it should stop. This allows you to distinguish between retryable failures (e.g., network timeouts) and permanent failures (e.g., invalid input). The function receives both the value and error from the action's result. Note that context cancellation errors will automatically stop retrying regardless of this function's return value.

Returns:

A ReaderResult[A] that, when executed with a context, will perform the retry
logic with context cancellation support and return the final (value, error) tuple.

Type Parameters:

  • A: The type of the success value

Context Cancellation:

The retry mechanism respects context cancellation in two ways:

  1. During retry delays: If the context is cancelled while waiting between retries, the operation stops immediately and returns the context error.
  2. During action execution: If the action itself checks the context and returns an error due to cancellation, the retry loop will stop (assuming the check function doesn't force a retry on context errors).

Implementation Details:

This function wraps the functional github.com/IBM/fp-go/v2/context/readerresult.Retrying by converting between the idiomatic (value, error) tuple representation and the functional Result[A] representation. The conversion is handled by ToReaderResult and FromReaderResult, ensuring seamless integration with the underlying retry mechanism that uses delayWithCancel to properly handle context cancellation during delays.

Example:

// Create a retry policy: exponential backoff with a cap, limited to 5 retries
policy := retry.Monoid.Concat(
    retry.LimitRetries(5),
    retry.CapDelay(10*time.Second, retry.ExponentialBackoff(100*time.Millisecond)),
)

// Action that fetches data, with retry status information
fetchData := func(status retry.RetryStatus) ReaderResult[string] {
    return func(ctx context.Context) (string, error) {
        // Check if context is cancelled
        if ctx.Err() != nil {
            return "", ctx.Err()
        }
        // Simulate an HTTP request that might fail
        if status.IterNumber < 3 {
            return "", fmt.Errorf("temporary error")
        }
        return "success", nil
    }
}

// Check function: retry on any error except context cancellation
shouldRetry := func(val string, err error) bool {
    return err != nil && !errors.Is(err, context.Canceled)
}

// Create the retrying computation
retryingFetch := Retrying(policy, fetchData, shouldRetry)

// Execute with a cancellable context
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
result, err := retryingFetch(ctx)

See also:

  • retry.RetryPolicy for available retry policies
  • retry.RetryStatus for information passed to the action
  • context.Context for context cancellation semantics
  • github.com/IBM/fp-go/v2/context/readerresult.Retrying for the underlying functional implementation
func Right[A any](a A) ReaderResult[A]

Right creates a ReaderResult that always succeeds with the given value.

This is the success constructor for ReaderResult, analogous to Either's Right. The resulting computation ignores the context and immediately returns the value.

Type Parameters:

  • A: The value type

Parameters:

  • a: The value to return

Returns:

  • A ReaderResult[A] that always succeeds with the given value
Example

ExampleRight demonstrates creating a ReaderResult that always succeeds with a value. This is the success constructor for ReaderResult, analogous to Either's Right.

rr := Right(42)
value, err := rr(context.Background())
fmt.Println(value, err)
Output:

42 <nil>

func RightReader

func RightReader[A any](rdr Reader[context.Context, A]) ReaderResult[A]

func SequenceArray

func SequenceArray[A any](ma []ReaderResult[A]) ReaderResult[[]A]

SequenceArray converts an array of ReaderResult values into a single ReaderResult of an array. If any element fails, the entire operation fails with the first error encountered. All computations share the same environment.

Example:

readers := []readerresult.ReaderResult[Config, int]{
    readerresult.Of[Config](1),
    readerresult.Of[Config](2),
    readerresult.Of[Config](3),
}
result := readerresult.SequenceArray(readers)
// result(cfg) returns ([]int{1, 2, 3}, nil)

func SequenceT1

func SequenceT1[A any](a ReaderResult[A]) ReaderResult[T.Tuple1[A]]

SequenceT1 wraps a single ReaderResult in a Tuple1.

This is mainly for consistency with the other SequenceT functions.

Type Parameters:

  • A: The value type

Parameters:

  • a: A ReaderResult[A]

Returns:

  • A ReaderResult[Tuple1[A]]

Example:

rr := readerresult.Right(42)
result := readerresult.SequenceT1(rr)
tuple, err := result(ctx)  // Returns (Tuple1{42}, nil)

func SequenceT2

func SequenceT2[A, B any](
	a ReaderResult[A],
	b ReaderResult[B],
) ReaderResult[T.Tuple2[A, B]]

SequenceT2 combines two independent ReaderResult computations into a tuple.

Both computations are executed with the same context. If either fails, the entire operation fails with the first error encountered.

Type Parameters:

  • A: The first value type
  • B: The second value type

Parameters:

  • a: The first ReaderResult
  • b: The second ReaderResult

Returns:

  • A ReaderResult[Tuple2[A, B]] containing both results

Example:

getUser := readerresult.Right(User{ID: 1})
getConfig := readerresult.Right(Config{Port: 8080})
result := readerresult.SequenceT2(getUser, getConfig)
tuple, err := result(ctx)  // Returns (Tuple2{User, Config}, nil)

func SequenceT3

func SequenceT3[A, B, C any](
	a ReaderResult[A],
	b ReaderResult[B],
	c ReaderResult[C],
) ReaderResult[T.Tuple3[A, B, C]]

SequenceT3 combines three independent ReaderResult computations into a tuple.

All computations are executed with the same context. If any fails, the entire operation fails with the first error encountered.

Type Parameters:

  • A: The first value type
  • B: The second value type
  • C: The third value type

Parameters:

  • a: The first ReaderResult
  • b: The second ReaderResult
  • c: The third ReaderResult

Returns:

  • A ReaderResult[Tuple3[A, B, C]] containing all three results

func SequenceT4

func SequenceT4[A, B, C, D any](
	a ReaderResult[A],
	b ReaderResult[B],
	c ReaderResult[C],
	d ReaderResult[D],
) ReaderResult[T.Tuple4[A, B, C, D]]

SequenceT4 combines four independent ReaderResult computations into a tuple.

All computations are executed with the same context. If any fails, the entire operation fails with the first error encountered.

Type Parameters:

  • A: The first value type
  • B: The second value type
  • C: The third value type
  • D: The fourth value type

Parameters:

  • a: The first ReaderResult
  • b: The second ReaderResult
  • c: The third ReaderResult
  • d: The fourth ReaderResult

Returns:

  • A ReaderResult[Tuple4[A, B, C, D]] containing all four results

func WithContext

func WithContext[A any](ma ReaderResult[A]) ReaderResult[A]

WithContext wraps an existing ReaderResult and performs a context check for cancellation before delegating to the underlying computation.

If the context has been cancelled (ctx.Err() != nil), it immediately returns an error containing the cancellation cause without executing the wrapped computation. Otherwise, it delegates to the original ReaderResult.

This is useful for adding cancellation checks to computations that may not check the context themselves, ensuring that cancelled operations fail fast.

Example:

// A computation that might take a long time
slowComputation := func(ctx context.Context) (int, error) {
    time.Sleep(5 * time.Second)
    return 42, nil
}

// Wrap it to check for cancellation before execution
safeSlow := readerresult.WithContext(slowComputation)

// If context is already cancelled, this returns immediately
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
result, err := safeSlow(ctx) // Returns error immediately without sleeping

type Result

type Result[A any] = result.Result[A]

Result represents an Either with error as the left type, compatible with Go's (value, error) tuple.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL