1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-12-17 23:37:41 +02:00
Files
fp-go/v2/idiomatic/context/readerresult/examples_bind_test.go
Dr. Carsten Leue fceda15701 doc: improve docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-17 10:11:58 +01:00

739 lines
24 KiB
Go

// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerresult
import (
"context"
"fmt"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/optics/lens"
RES "github.com/IBM/fp-go/v2/result"
)
// Post represents a blog post
// fp-go:Lens
type Post struct {
ID int
UserID int
Title string
}
// State represents accumulated state in do-notation
// fp-go:Lens
type State struct {
User User
Posts []Post
FullName string
Status string
}
// getUser simulates fetching a user by ID
func getUser(id int) ReaderResult[User] {
return func(ctx context.Context) (User, error) {
return User{ID: id, Name: "Alice"}, nil
}
}
// getPosts simulates fetching posts for a user
func getPosts(userID int) ReaderResult[[]Post] {
return func(ctx context.Context) ([]Post, error) {
return []Post{
{ID: 1, UserID: userID, Title: "First Post"},
{ID: 2, UserID: userID, Title: "Second Post"},
}, nil
}
}
// fp-go:Lens
type SimpleState struct {
Value int
}
// 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.
func ExampleDo() {
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>
}
// 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
func ExampleBind() {
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>
}
// fp-go:Lens
type NameState struct {
FirstName string
LastName string
FullName string
}
// 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
func ExampleLet() {
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>
}
// fp-go:Lens
type StatusState struct {
Status string
}
// 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.
func ExampleLetTo() {
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>
}
// fp-go:Lens
type UserState struct {
User User
}
// 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.
func ExampleBindTo() {
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>
}
// fp-go:Lens
type ConfigState struct {
Config string
}
// ExampleBindReaderK demonstrates binding a Reader computation (context-dependent
// but error-free) into a ReaderResult do-notation chain.
func ExampleBindReaderK() {
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>
}
// fp-go:Lens
type NumberState struct {
Number int
}
// 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.
func ExampleBindEitherK() {
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>
}
// fp-go:Lens
type DataState struct {
Data string
}
// 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)
func ExampleBindResultK() {
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>
}
// fp-go:Lens
type RequestState struct {
RequestID string
}
// ExampleBindToReader demonstrates converting a Reader computation into a
// ReaderResult and binding it to create an initial state.
func ExampleBindToReader() {
// 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>
}
// fp-go:Lens
type ValueState struct {
Value int
}
// ExampleBindToEither demonstrates converting a Result (Either) into a
// ReaderResult and binding it to create an initial state.
func ExampleBindToEither() {
// 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>
}
// fp-go:Lens
type ResultState struct {
Result string
}
// ExampleBindToResult demonstrates converting an idiomatic Go tuple (value, error)
// into a ReaderResult and binding it to create an initial state.
func ExampleBindToResult() {
// 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>
}
// fp-go:Lens
type EnvState struct {
Environment string
}
// ExampleApReaderS demonstrates applying a Reader computation in applicative style,
// combining it with the current state in a do-notation chain.
func ExampleApReaderS() {
// 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>
}
// fp-go:Lens
type ScoreState struct {
Score int
}
// ExampleApEitherS demonstrates applying a Result (Either) in applicative style,
// combining it with the current state in a do-notation chain.
func ExampleApEitherS() {
// 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>
}
// fp-go:Lens
type MessageState struct {
Message string
}
// ExampleApResultS demonstrates applying an idiomatic Go tuple (value, error)
// in applicative style, combining it with the current state in a do-notation chain.
func ExampleApResultS() {
// 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>
}