mirror of
https://github.com/IBM/fp-go.git
synced 2026-03-10 13:31:01 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6d30bb642 | ||
|
|
1821f00fbe |
130
v2/context/reader/reader.go
Normal file
130
v2/context/reader/reader.go
Normal file
@@ -0,0 +1,130 @@
|
||||
// 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 reader provides a specialization of the Reader monad for [context.Context].
|
||||
//
|
||||
// This package offers a context-aware Reader monad that simplifies working with
|
||||
// Go's [context.Context] in a functional programming style. It eliminates the need
|
||||
// to explicitly thread context through function calls while maintaining type safety
|
||||
// and composability.
|
||||
//
|
||||
// # Core Concept
|
||||
//
|
||||
// The Reader monad represents computations that depend on a shared environment.
|
||||
// In this package, that environment is fixed to [context.Context], making it
|
||||
// particularly useful for:
|
||||
//
|
||||
// - Request-scoped data propagation
|
||||
// - Cancellation and timeout handling
|
||||
// - Dependency injection via context values
|
||||
// - Avoiding explicit context parameter threading
|
||||
//
|
||||
// # Type Definitions
|
||||
//
|
||||
// - Reader[A]: A computation that depends on context.Context and produces A
|
||||
// - Kleisli[A, B]: A function from A to Reader[B] for composing computations
|
||||
// - Operator[A, B]: A transformation from Reader[A] to Reader[B]
|
||||
//
|
||||
// # Usage Pattern
|
||||
//
|
||||
// Instead of passing context explicitly through every function:
|
||||
//
|
||||
// func processUser(ctx context.Context, userID string) (User, error) {
|
||||
// user := fetchUser(ctx, userID)
|
||||
// profile := fetchProfile(ctx, user.ProfileID)
|
||||
// return enrichUser(ctx, user, profile), nil
|
||||
// }
|
||||
//
|
||||
// You can use Reader to compose context-dependent operations:
|
||||
//
|
||||
// fetchUser := func(userID string) Reader[User] {
|
||||
// return func(ctx context.Context) User {
|
||||
// // Use ctx for database access, cancellation, etc.
|
||||
// return queryDatabase(ctx, userID)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// processUser := func(userID string) Reader[User] {
|
||||
// return F.Pipe2(
|
||||
// fetchUser(userID),
|
||||
// reader.Chain(func(user User) Reader[Profile] {
|
||||
// return fetchProfile(user.ProfileID)
|
||||
// }),
|
||||
// reader.Map(func(profile Profile) User {
|
||||
// return enrichUser(user, profile)
|
||||
// }),
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// // Execute with context
|
||||
// ctx := context.Background()
|
||||
// user := processUser("user123")(ctx)
|
||||
//
|
||||
// # Integration with Standard Library
|
||||
//
|
||||
// This package works seamlessly with Go's standard [context] package:
|
||||
//
|
||||
// - Context cancellation and deadlines are preserved
|
||||
// - Context values can be accessed within Reader computations
|
||||
// - Readers can be composed with context-aware libraries
|
||||
//
|
||||
// # Relationship to Other Packages
|
||||
//
|
||||
// This package is a specialization of [github.com/IBM/fp-go/v2/reader] where
|
||||
// the environment type R is fixed to [context.Context]. For more general
|
||||
// Reader operations, see the base reader package.
|
||||
//
|
||||
// For combining Reader with other monads:
|
||||
// - [github.com/IBM/fp-go/v2/context/readerio]: Reader + IO effects
|
||||
// - [github.com/IBM/fp-go/v2/readeroption]: Reader + Option
|
||||
// - [github.com/IBM/fp-go/v2/readerresult]: Reader + Result (Either)
|
||||
//
|
||||
// # Example: HTTP Request Handler
|
||||
//
|
||||
// type RequestContext struct {
|
||||
// UserID string
|
||||
// RequestID string
|
||||
// }
|
||||
//
|
||||
// // Extract request context from context.Context
|
||||
// getRequestContext := func(ctx context.Context) RequestContext {
|
||||
// return RequestContext{
|
||||
// UserID: ctx.Value("userID").(string),
|
||||
// RequestID: ctx.Value("requestID").(string),
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // A Reader that logs with request context
|
||||
// logInfo := func(message string) Reader[function.Void] {
|
||||
// return func(ctx context.Context) function.Void {
|
||||
// reqCtx := getRequestContext(ctx)
|
||||
// log.Printf("[%s] User %s: %s", reqCtx.RequestID, reqCtx.UserID, message)
|
||||
// return function.VOID
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Compose operations
|
||||
// handleRequest := func(data string) Reader[Response] {
|
||||
// return F.Pipe2(
|
||||
// logInfo("Processing request"),
|
||||
// reader.Chain(func(_ function.Void) Reader[Result] {
|
||||
// return processData(data)
|
||||
// }),
|
||||
// reader.Map(func(result Result) Response {
|
||||
// return Response{Data: result}
|
||||
// }),
|
||||
// )
|
||||
// }
|
||||
package reader
|
||||
142
v2/context/reader/types.go
Normal file
142
v2/context/reader/types.go
Normal file
@@ -0,0 +1,142 @@
|
||||
// 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 reader
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
R "github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
type (
|
||||
// Reader represents a computation that depends on a [context.Context] and produces a value of type A.
|
||||
//
|
||||
// This is a specialization of the generic Reader monad where the environment type is fixed
|
||||
// to [context.Context]. This is particularly useful for Go applications that need to thread
|
||||
// context through computations for cancellation, deadlines, and request-scoped values.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The result type produced by the computation
|
||||
//
|
||||
// Reader[A] is equivalent to func(context.Context) A
|
||||
//
|
||||
// The Reader monad enables:
|
||||
// - Dependency injection using context values
|
||||
// - Cancellation and timeout handling
|
||||
// - Request-scoped data propagation
|
||||
// - Avoiding explicit context parameter threading
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // A Reader that extracts a user ID from context
|
||||
// getUserID := func(ctx context.Context) string {
|
||||
// if userID, ok := ctx.Value("userID").(string); ok {
|
||||
// return userID
|
||||
// }
|
||||
// return "anonymous"
|
||||
// }
|
||||
//
|
||||
// // A Reader that checks if context is cancelled
|
||||
// isCancelled := func(ctx context.Context) bool {
|
||||
// select {
|
||||
// case <-ctx.Done():
|
||||
// return true
|
||||
// default:
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Use the readers with a context
|
||||
// ctx := context.WithValue(context.Background(), "userID", "user123")
|
||||
// userID := getUserID(ctx) // "user123"
|
||||
// cancelled := isCancelled(ctx) // false
|
||||
Reader[A any] = R.Reader[context.Context, A]
|
||||
|
||||
// Kleisli represents a Kleisli arrow for the context-based Reader monad.
|
||||
//
|
||||
// It's a function from A to Reader[B], used for composing Reader computations
|
||||
// that all depend on the same [context.Context].
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The input type
|
||||
// - B: The output type wrapped in Reader
|
||||
//
|
||||
// Kleisli[A, B] is equivalent to func(A) func(context.Context) B
|
||||
//
|
||||
// Kleisli arrows are fundamental for monadic composition, allowing you to chain
|
||||
// operations that depend on context without explicitly passing the context through
|
||||
// each function call.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // A Kleisli arrow that creates a greeting Reader from a name
|
||||
// greet := func(name string) Reader[string] {
|
||||
// return func(ctx context.Context) string {
|
||||
// if deadline, ok := ctx.Deadline(); ok {
|
||||
// return fmt.Sprintf("Hello %s (deadline: %v)", name, deadline)
|
||||
// }
|
||||
// return fmt.Sprintf("Hello %s", name)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Use the Kleisli arrow
|
||||
// ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
// defer cancel()
|
||||
// greeting := greet("Alice")(ctx) // "Hello Alice (deadline: ...)"
|
||||
Kleisli[A, B any] = R.Reader[A, Reader[B]]
|
||||
|
||||
// Operator represents a transformation from one Reader to another.
|
||||
//
|
||||
// It takes a Reader[A] and produces a Reader[B], where both readers depend on
|
||||
// the same [context.Context]. This type is commonly used for operations like
|
||||
// Map, Chain, and other transformations that convert readers while preserving
|
||||
// the context dependency.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The input Reader's result type
|
||||
// - B: The output Reader's result type
|
||||
//
|
||||
// Operator[A, B] is equivalent to func(Reader[A]) func(context.Context) B
|
||||
//
|
||||
// Operators enable building pipelines of context-dependent computations where
|
||||
// each step can transform the result of the previous computation while maintaining
|
||||
// access to the shared context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // An operator that transforms int readers to string readers
|
||||
// intToString := func(r Reader[int]) Reader[string] {
|
||||
// return func(ctx context.Context) string {
|
||||
// value := r(ctx)
|
||||
// return strconv.Itoa(value)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // A Reader that extracts a timeout value from context
|
||||
// getTimeout := func(ctx context.Context) int {
|
||||
// if deadline, ok := ctx.Deadline(); ok {
|
||||
// return int(time.Until(deadline).Seconds())
|
||||
// }
|
||||
// return 0
|
||||
// }
|
||||
//
|
||||
// // Transform the Reader
|
||||
// getTimeoutStr := intToString(getTimeout)
|
||||
// ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
// defer cancel()
|
||||
// result := getTimeoutStr(ctx) // "30" (approximately)
|
||||
Operator[A, B any] = Kleisli[Reader[A], B]
|
||||
)
|
||||
@@ -3,6 +3,7 @@ package readerreaderioresult
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/reader"
|
||||
"github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
@@ -196,6 +197,65 @@ func LocalReaderIOResultK[A, R1, R2 any](f readerioresult.Kleisli[R2, R1]) func(
|
||||
return RRIOE.LocalReaderIOEitherK[A](f)
|
||||
}
|
||||
|
||||
// LocalReaderK transforms the outer environment of a ReaderReaderIOResult using a Reader-based Kleisli arrow.
|
||||
// It allows you to modify the outer environment through a pure computation that depends on the inner context
|
||||
// before passing it to the ReaderReaderIOResult.
|
||||
//
|
||||
// This is useful when the outer environment transformation is a pure computation that requires access
|
||||
// to the inner context (e.g., context.Context) but cannot fail. Common use cases include:
|
||||
// - Extracting configuration from context values
|
||||
// - Computing derived environment values based on context
|
||||
// - Transforming environment based on context metadata
|
||||
//
|
||||
// The transformation happens in two stages:
|
||||
// 1. The Reader function f is executed with the R2 outer environment and inner context to produce an R1 value
|
||||
// 2. The resulting R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type produced by the ReaderReaderIOResult
|
||||
// - R1: The original outer environment type expected by the ReaderReaderIOResult
|
||||
// - R2: The new input outer environment type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Reader Kleisli arrow that transforms R2 to R1 using the inner context
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
|
||||
//
|
||||
// Example Usage:
|
||||
//
|
||||
// type ctxKey string
|
||||
// const configKey ctxKey = "config"
|
||||
//
|
||||
// // Extract config from context and transform environment
|
||||
// extractConfig := func(path string) reader.Reader[DetailedConfig] {
|
||||
// return func(ctx context.Context) DetailedConfig {
|
||||
// if cfg, ok := ctx.Value(configKey).(DetailedConfig); ok {
|
||||
// return cfg
|
||||
// }
|
||||
// return DetailedConfig{Host: "localhost", Port: 8080}
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Use the config
|
||||
// useConfig := func(cfg DetailedConfig) readerioresult.ReaderIOResult[string] {
|
||||
// return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
// return func() result.Result[string] {
|
||||
// return result.Of(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Compose using LocalReaderK
|
||||
// adapted := LocalReaderK[string](extractConfig)(useConfig)
|
||||
// ctx := context.WithValue(context.Background(), configKey, DetailedConfig{Host: "api.example.com", Port: 443})
|
||||
// result := adapted("config.json")(ctx)() // Result: "api.example.com:443"
|
||||
//
|
||||
//go:inline
|
||||
func LocalReaderK[A, R1, R2 any](f reader.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
|
||||
return RRIOE.LocalReaderK[error, A](f)
|
||||
}
|
||||
|
||||
// LocalReaderReaderIOEitherK transforms the outer environment of a ReaderReaderIOResult using a ReaderReaderIOResult-based Kleisli arrow.
|
||||
// It allows you to modify the outer environment through a computation that depends on both the outer environment
|
||||
// and the inner context, and can perform IO effects that may fail.
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/reader"
|
||||
"github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
@@ -426,3 +427,226 @@ func TestLocalReaderIOResultK(t *testing.T) {
|
||||
assert.True(t, result.IsLeft(resErr))
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalReaderK tests LocalReaderK functionality
|
||||
func TestLocalReaderK(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("basic Reader transformation", func(t *testing.T) {
|
||||
// Reader that transforms string path to SimpleConfig using context
|
||||
loadConfig := func(path string) reader.Reader[SimpleConfig] {
|
||||
return func(ctx context.Context) SimpleConfig {
|
||||
// Could extract values from context here
|
||||
return SimpleConfig{Port: 8080}
|
||||
}
|
||||
}
|
||||
|
||||
// ReaderReaderIOResult that uses the config
|
||||
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose using LocalReaderK
|
||||
adapted := LocalReaderK[string](loadConfig)(useConfig)
|
||||
res := adapted("config.json")(ctx)()
|
||||
|
||||
assert.Equal(t, result.Of("Port: 8080"), res)
|
||||
})
|
||||
|
||||
t.Run("extract config from context", func(t *testing.T) {
|
||||
type ctxKey string
|
||||
const configKey ctxKey = "config"
|
||||
|
||||
// Reader that extracts config from context
|
||||
extractConfig := func(path string) reader.Reader[DetailedConfig] {
|
||||
return func(ctx context.Context) DetailedConfig {
|
||||
if cfg, ok := ctx.Value(configKey).(DetailedConfig); ok {
|
||||
return cfg
|
||||
}
|
||||
// Default config if not in context
|
||||
return DetailedConfig{Host: "localhost", Port: 8080}
|
||||
}
|
||||
}
|
||||
|
||||
// Use the config
|
||||
useConfig := func(cfg DetailedConfig) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalReaderK[string](extractConfig)(useConfig)
|
||||
|
||||
// With context value
|
||||
ctxWithConfig := context.WithValue(ctx, configKey, DetailedConfig{Host: "api.example.com", Port: 443})
|
||||
res := adapted("ignored")(ctxWithConfig)()
|
||||
assert.Equal(t, result.Of("api.example.com:443"), res)
|
||||
|
||||
// Without context value (uses default)
|
||||
resDefault := adapted("ignored")(ctx)()
|
||||
assert.Equal(t, result.Of("localhost:8080"), resDefault)
|
||||
})
|
||||
|
||||
t.Run("context-aware transformation", func(t *testing.T) {
|
||||
type ctxKey string
|
||||
const multiplierKey ctxKey = "multiplier"
|
||||
|
||||
// Reader that uses context to compute environment
|
||||
computeValue := func(base int) reader.Reader[int] {
|
||||
return func(ctx context.Context) int {
|
||||
if mult, ok := ctx.Value(multiplierKey).(int); ok {
|
||||
return base * mult
|
||||
}
|
||||
return base
|
||||
}
|
||||
}
|
||||
|
||||
// Use the computed value
|
||||
formatValue := func(val int) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("Value: %d", val))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalReaderK[string](computeValue)(formatValue)
|
||||
|
||||
// With multiplier in context
|
||||
ctxWithMult := context.WithValue(ctx, multiplierKey, 10)
|
||||
res := adapted(5)(ctxWithMult)()
|
||||
assert.Equal(t, result.Of("Value: 50"), res)
|
||||
|
||||
// Without multiplier (uses base value)
|
||||
resBase := adapted(5)(ctx)()
|
||||
assert.Equal(t, result.Of("Value: 5"), resBase)
|
||||
})
|
||||
|
||||
t.Run("compose multiple LocalReaderK", func(t *testing.T) {
|
||||
type ctxKey string
|
||||
const prefixKey ctxKey = "prefix"
|
||||
|
||||
// First transformation: int -> string using context
|
||||
intToString := func(n int) reader.Reader[string] {
|
||||
return func(ctx context.Context) string {
|
||||
if prefix, ok := ctx.Value(prefixKey).(string); ok {
|
||||
return fmt.Sprintf("%s-%d", prefix, n)
|
||||
}
|
||||
return fmt.Sprintf("%d", n)
|
||||
}
|
||||
}
|
||||
|
||||
// Second transformation: string -> SimpleConfig
|
||||
stringToConfig := func(s string) reader.Reader[SimpleConfig] {
|
||||
return func(ctx context.Context) SimpleConfig {
|
||||
return SimpleConfig{Port: len(s) * 100}
|
||||
}
|
||||
}
|
||||
|
||||
// Use the config
|
||||
formatConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose transformations
|
||||
step1 := LocalReaderK[string](stringToConfig)(formatConfig)
|
||||
step2 := LocalReaderK[string](intToString)(step1)
|
||||
|
||||
// With prefix in context
|
||||
ctxWithPrefix := context.WithValue(ctx, prefixKey, "test")
|
||||
res := step2(42)(ctxWithPrefix)()
|
||||
// "test-42" has length 7, so port = 700
|
||||
assert.Equal(t, result.Of("Port: 700"), res)
|
||||
|
||||
// Without prefix
|
||||
resNoPrefix := step2(42)(ctx)()
|
||||
// "42" has length 2, so port = 200
|
||||
assert.Equal(t, result.Of("Port: 200"), resNoPrefix)
|
||||
})
|
||||
|
||||
t.Run("error propagation in ReaderReaderIOResult", func(t *testing.T) {
|
||||
// Reader transformation (pure, cannot fail)
|
||||
loadConfig := func(path string) reader.Reader[SimpleConfig] {
|
||||
return func(ctx context.Context) SimpleConfig {
|
||||
return SimpleConfig{Port: 8080}
|
||||
}
|
||||
}
|
||||
|
||||
// ReaderReaderIOResult that returns an error
|
||||
failingOperation := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Left[string](errors.New("operation failed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalReaderK[string](loadConfig)(failingOperation)
|
||||
res := adapted("config.json")(ctx)()
|
||||
|
||||
// Error from the ReaderReaderIOResult should propagate
|
||||
assert.True(t, result.IsLeft(res))
|
||||
})
|
||||
|
||||
t.Run("real-world: environment selection based on context", func(t *testing.T) {
|
||||
type Environment string
|
||||
const (
|
||||
Dev Environment = "dev"
|
||||
Prod Environment = "prod"
|
||||
)
|
||||
|
||||
type ctxKey string
|
||||
const envKey ctxKey = "environment"
|
||||
|
||||
type EnvConfig struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// Reader that selects config based on context environment
|
||||
selectConfig := func(envName EnvConfig) reader.Reader[DetailedConfig] {
|
||||
return func(ctx context.Context) DetailedConfig {
|
||||
env := Dev
|
||||
if e, ok := ctx.Value(envKey).(Environment); ok {
|
||||
env = e
|
||||
}
|
||||
|
||||
switch env {
|
||||
case Prod:
|
||||
return DetailedConfig{Host: "api.production.com", Port: 443}
|
||||
default:
|
||||
return DetailedConfig{Host: "localhost", Port: 8080}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use the selected config
|
||||
useConfig := func(cfg DetailedConfig) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("Connecting to %s:%d", cfg.Host, cfg.Port))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalReaderK[string](selectConfig)(useConfig)
|
||||
|
||||
// Production environment
|
||||
ctxProd := context.WithValue(ctx, envKey, Prod)
|
||||
resProd := adapted(EnvConfig{Name: "app"})(ctxProd)()
|
||||
assert.Equal(t, result.Of("Connecting to api.production.com:443"), resProd)
|
||||
|
||||
// Development environment (default)
|
||||
resDev := adapted(EnvConfig{Name: "app"})(ctx)()
|
||||
assert.Equal(t, result.Of("Connecting to localhost:8080"), resDev)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/context/reader"
|
||||
thunk "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
@@ -267,10 +268,89 @@ func LocalThunkK[A, C1, C2 any](f thunk.Kleisli[C2, C1]) func(Effect[C1, A]) Eff
|
||||
// - Local/Contramap: Pure context transformation (C2 -> C1)
|
||||
// - LocalIOK: IO-based transformation (C2 -> IO[C1])
|
||||
// - LocalIOResultK: IO with error handling (C2 -> IOResult[C1])
|
||||
// - LocalReaderIOResultK: Reader-based with IO and errors (C2 -> ReaderIOResult[C1])
|
||||
// - LocalThunkK: Reader-based with IO and errors (C2 -> ReaderIOResult[C1])
|
||||
// - LocalEffectK: Full Effect transformation (C2 -> Effect[C2, C1])
|
||||
//
|
||||
//go:inline
|
||||
func LocalEffectK[A, C1, C2 any](f Kleisli[C2, C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
|
||||
return readerreaderioresult.LocalReaderReaderIOEitherK[A](f)
|
||||
}
|
||||
|
||||
// LocalReaderK transforms the context of an Effect using a Reader-based Kleisli arrow.
|
||||
// It allows you to modify the context through a pure computation that depends on the runtime context
|
||||
// before passing it to the Effect.
|
||||
//
|
||||
// This is useful when the context transformation is a pure computation that requires access
|
||||
// to the runtime context (context.Context) but cannot fail. Common use cases include:
|
||||
// - Extracting configuration from context values
|
||||
// - Computing derived context values based on runtime context
|
||||
// - Transforming context based on runtime metadata
|
||||
//
|
||||
// The transformation happens in two stages:
|
||||
// 1. The Reader function f is executed with the C2 context and runtime context to produce a C1 value
|
||||
// 2. The resulting C1 value is passed as the context to the Effect[C1, A]
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The value type produced by the effect
|
||||
// - C1: The inner context type (required by the original effect)
|
||||
// - C2: The outer context type (provided to the transformed effect)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A Reader Kleisli arrow that transforms C2 to C1 using the runtime context
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - func(Effect[C1, A]) Effect[C2, A]: A function that adapts the effect to use C2
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// type ctxKey string
|
||||
// const configKey ctxKey = "config"
|
||||
//
|
||||
// type DetailedConfig struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
//
|
||||
// type SimpleConfig struct {
|
||||
// Port int
|
||||
// }
|
||||
//
|
||||
// // Extract config from runtime context and transform
|
||||
// extractConfig := func(path string) reader.Reader[DetailedConfig] {
|
||||
// return func(ctx context.Context) DetailedConfig {
|
||||
// if cfg, ok := ctx.Value(configKey).(DetailedConfig); ok {
|
||||
// return cfg
|
||||
// }
|
||||
// return DetailedConfig{Host: "localhost", Port: 8080}
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Effect that uses DetailedConfig
|
||||
// configEffect := effect.Of[DetailedConfig]("connected")
|
||||
//
|
||||
// // Transform to use string path instead
|
||||
// transform := effect.LocalReaderK[string](extractConfig)
|
||||
// pathEffect := transform(configEffect)
|
||||
//
|
||||
// // Run with runtime context containing config
|
||||
// ctx := context.WithValue(context.Background(), configKey, DetailedConfig{Host: "api.example.com", Port: 443})
|
||||
// ioResult := effect.Provide[string]("config.json")(pathEffect)
|
||||
// readerResult := effect.RunSync(ioResult)
|
||||
// result, err := readerResult(ctx) // Uses config from context
|
||||
//
|
||||
// # Comparison with other Local functions
|
||||
//
|
||||
// - Local/Contramap: Pure context transformation (C2 -> C1)
|
||||
// - LocalIOK: IO-based transformation (C2 -> IO[C1])
|
||||
// - LocalIOResultK: IO with error handling (C2 -> IOResult[C1])
|
||||
// - LocalReaderK: Reader-based pure transformation with runtime context access (C2 -> Reader[C1])
|
||||
// - LocalThunkK: Reader-based with IO and errors (C2 -> ReaderIOResult[C1])
|
||||
// - LocalEffectK: Full Effect transformation (C2 -> Effect[C2, C1])
|
||||
//
|
||||
//go:inline
|
||||
func LocalReaderK[A, C1, C2 any](f reader.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
|
||||
return readerreaderioresult.LocalReaderK[A](f)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,9 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/reader"
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -618,3 +620,347 @@ func TestLocalEffectK(t *testing.T) {
|
||||
assert.Equal(t, 60, result) // 3 * 10 * 2
|
||||
})
|
||||
}
|
||||
|
||||
func TestLocalReaderK(t *testing.T) {
|
||||
t.Run("basic Reader transformation", func(t *testing.T) {
|
||||
type SimpleConfig struct {
|
||||
Port int
|
||||
}
|
||||
|
||||
// Reader that transforms string path to SimpleConfig using runtime context
|
||||
loadConfig := func(path string) reader.Reader[SimpleConfig] {
|
||||
return func(ctx context.Context) SimpleConfig {
|
||||
// Could extract values from runtime context here
|
||||
return SimpleConfig{Port: 8080}
|
||||
}
|
||||
}
|
||||
|
||||
// Effect that uses the config
|
||||
configEffect := Of[SimpleConfig]("connected")
|
||||
|
||||
// Transform using LocalReaderK
|
||||
transform := LocalReaderK[string](loadConfig)
|
||||
pathEffect := transform(configEffect)
|
||||
|
||||
// Run with path
|
||||
ioResult := Provide[string]("config.json")(pathEffect)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "connected", result)
|
||||
})
|
||||
|
||||
t.Run("extract config from runtime context", func(t *testing.T) {
|
||||
type ctxKey string
|
||||
const configKey ctxKey = "config"
|
||||
|
||||
type DetailedConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
// Reader that extracts config from runtime context
|
||||
extractConfig := func(path string) reader.Reader[DetailedConfig] {
|
||||
return func(ctx context.Context) DetailedConfig {
|
||||
if cfg, ok := ctx.Value(configKey).(DetailedConfig); ok {
|
||||
return cfg
|
||||
}
|
||||
// Default config if not in runtime context
|
||||
return DetailedConfig{Host: "localhost", Port: 8080}
|
||||
}
|
||||
}
|
||||
|
||||
// Effect that uses the config
|
||||
configEffect := Chain(func(cfg DetailedConfig) Effect[DetailedConfig, string] {
|
||||
return Of[DetailedConfig](fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
|
||||
})(readerreaderioresult.Ask[DetailedConfig]())
|
||||
|
||||
transform := LocalReaderK[string](extractConfig)
|
||||
pathEffect := transform(configEffect)
|
||||
|
||||
// With config in runtime context
|
||||
ctxWithConfig := context.WithValue(context.Background(), configKey, DetailedConfig{Host: "api.example.com", Port: 443})
|
||||
ioResult := Provide[string]("ignored")(pathEffect)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(ctxWithConfig)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "api.example.com:443", result)
|
||||
|
||||
// Without config in runtime context (uses default)
|
||||
ioResult2 := Provide[string]("ignored")(pathEffect)
|
||||
readerResult2 := RunSync(ioResult2)
|
||||
result2, err2 := readerResult2(context.Background())
|
||||
|
||||
assert.NoError(t, err2)
|
||||
assert.Equal(t, "localhost:8080", result2)
|
||||
})
|
||||
|
||||
t.Run("runtime context-aware transformation", func(t *testing.T) {
|
||||
type ctxKey string
|
||||
const multiplierKey ctxKey = "multiplier"
|
||||
|
||||
// Reader that uses runtime context to compute context
|
||||
computeValue := func(base int) reader.Reader[int] {
|
||||
return func(ctx context.Context) int {
|
||||
if mult, ok := ctx.Value(multiplierKey).(int); ok {
|
||||
return base * mult
|
||||
}
|
||||
return base
|
||||
}
|
||||
}
|
||||
|
||||
// Effect that uses the computed value
|
||||
valueEffect := Chain(func(val int) Effect[int, string] {
|
||||
return Of[int](fmt.Sprintf("Value: %d", val))
|
||||
})(readerreaderioresult.Ask[int]())
|
||||
|
||||
transform := LocalReaderK[string](computeValue)
|
||||
baseEffect := transform(valueEffect)
|
||||
|
||||
// With multiplier in runtime context
|
||||
ctxWithMult := context.WithValue(context.Background(), multiplierKey, 10)
|
||||
ioResult := Provide[string](5)(baseEffect)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(ctxWithMult)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Value: 50", result)
|
||||
|
||||
// Without multiplier (uses base value)
|
||||
ioResult2 := Provide[string](5)(baseEffect)
|
||||
readerResult2 := RunSync(ioResult2)
|
||||
result2, err2 := readerResult2(context.Background())
|
||||
|
||||
assert.NoError(t, err2)
|
||||
assert.Equal(t, "Value: 5", result2)
|
||||
})
|
||||
|
||||
t.Run("compose multiple LocalReaderK", func(t *testing.T) {
|
||||
type ctxKey string
|
||||
const prefixKey ctxKey = "prefix"
|
||||
|
||||
// First transformation: int -> string using runtime context
|
||||
intToString := func(n int) reader.Reader[string] {
|
||||
return func(ctx context.Context) string {
|
||||
if prefix, ok := ctx.Value(prefixKey).(string); ok {
|
||||
return fmt.Sprintf("%s-%d", prefix, n)
|
||||
}
|
||||
return fmt.Sprintf("%d", n)
|
||||
}
|
||||
}
|
||||
|
||||
// Second transformation: string -> SimpleConfig
|
||||
type SimpleConfig struct {
|
||||
Port int
|
||||
}
|
||||
|
||||
stringToConfig := func(s string) reader.Reader[SimpleConfig] {
|
||||
return func(ctx context.Context) SimpleConfig {
|
||||
return SimpleConfig{Port: len(s) * 100}
|
||||
}
|
||||
}
|
||||
|
||||
// Effect that uses the config
|
||||
configEffect := Chain(func(cfg SimpleConfig) Effect[SimpleConfig, string] {
|
||||
return Of[SimpleConfig](fmt.Sprintf("Port: %d", cfg.Port))
|
||||
})(readerreaderioresult.Ask[SimpleConfig]())
|
||||
|
||||
// Compose transformations
|
||||
step1 := LocalReaderK[string](stringToConfig)
|
||||
step2 := LocalReaderK[string](intToString)
|
||||
|
||||
effect1 := step1(configEffect)
|
||||
effect2 := step2(effect1)
|
||||
|
||||
// With prefix in runtime context
|
||||
ctxWithPrefix := context.WithValue(context.Background(), prefixKey, "test")
|
||||
ioResult := Provide[string](42)(effect2)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(ctxWithPrefix)
|
||||
|
||||
assert.NoError(t, err)
|
||||
// "test-42" has length 7, so port = 700
|
||||
assert.Equal(t, "Port: 700", result)
|
||||
|
||||
// Without prefix
|
||||
ioResult2 := Provide[string](42)(effect2)
|
||||
readerResult2 := RunSync(ioResult2)
|
||||
result2, err2 := readerResult2(context.Background())
|
||||
|
||||
assert.NoError(t, err2)
|
||||
// "42" has length 2, so port = 200
|
||||
assert.Equal(t, "Port: 200", result2)
|
||||
})
|
||||
|
||||
t.Run("error propagation from Effect", func(t *testing.T) {
|
||||
type SimpleConfig struct {
|
||||
Port int
|
||||
}
|
||||
|
||||
// Reader transformation (pure, cannot fail)
|
||||
loadConfig := func(path string) reader.Reader[SimpleConfig] {
|
||||
return func(ctx context.Context) SimpleConfig {
|
||||
return SimpleConfig{Port: 8080}
|
||||
}
|
||||
}
|
||||
|
||||
// Effect that returns an error
|
||||
expectedErr := assert.AnError
|
||||
failingEffect := Fail[SimpleConfig, string](expectedErr)
|
||||
|
||||
transform := LocalReaderK[string](loadConfig)
|
||||
pathEffect := transform(failingEffect)
|
||||
|
||||
ioResult := Provide[string]("config.json")(pathEffect)
|
||||
readerResult := RunSync(ioResult)
|
||||
_, err := readerResult(context.Background())
|
||||
|
||||
// Error from the Effect should propagate
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("real-world: environment selection based on runtime context", func(t *testing.T) {
|
||||
type Environment string
|
||||
const (
|
||||
Dev Environment = "dev"
|
||||
Prod Environment = "prod"
|
||||
)
|
||||
|
||||
type ctxKey string
|
||||
const envKey ctxKey = "environment"
|
||||
|
||||
type EnvConfig struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
type DetailedConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
// Reader that selects config based on runtime context environment
|
||||
selectConfig := func(envName EnvConfig) reader.Reader[DetailedConfig] {
|
||||
return func(ctx context.Context) DetailedConfig {
|
||||
env := Dev
|
||||
if e, ok := ctx.Value(envKey).(Environment); ok {
|
||||
env = e
|
||||
}
|
||||
|
||||
switch env {
|
||||
case Prod:
|
||||
return DetailedConfig{Host: "api.production.com", Port: 443}
|
||||
default:
|
||||
return DetailedConfig{Host: "localhost", Port: 8080}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Effect that uses the selected config
|
||||
configEffect := Chain(func(cfg DetailedConfig) Effect[DetailedConfig, string] {
|
||||
return Of[DetailedConfig](fmt.Sprintf("Connecting to %s:%d", cfg.Host, cfg.Port))
|
||||
})(readerreaderioresult.Ask[DetailedConfig]())
|
||||
|
||||
transform := LocalReaderK[string](selectConfig)
|
||||
envEffect := transform(configEffect)
|
||||
|
||||
// Production environment
|
||||
ctxProd := context.WithValue(context.Background(), envKey, Prod)
|
||||
ioResult := Provide[string](EnvConfig{Name: "app"})(envEffect)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(ctxProd)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Connecting to api.production.com:443", result)
|
||||
|
||||
// Development environment (default)
|
||||
ioResult2 := Provide[string](EnvConfig{Name: "app"})(envEffect)
|
||||
readerResult2 := RunSync(ioResult2)
|
||||
result2, err2 := readerResult2(context.Background())
|
||||
|
||||
assert.NoError(t, err2)
|
||||
assert.Equal(t, "Connecting to localhost:8080", result2)
|
||||
})
|
||||
|
||||
t.Run("composes with other Local functions", func(t *testing.T) {
|
||||
type Level1 struct {
|
||||
Value string
|
||||
}
|
||||
type Level2 struct {
|
||||
Data string
|
||||
}
|
||||
type Level3 struct {
|
||||
Info string
|
||||
}
|
||||
|
||||
// Effect at deepest level
|
||||
effect3 := Of[Level3]("result")
|
||||
|
||||
// Use LocalReaderK for first transformation (with runtime context access)
|
||||
localReaderK23 := LocalReaderK[string](func(l2 Level2) reader.Reader[Level3] {
|
||||
return func(ctx context.Context) Level3 {
|
||||
return Level3{Info: l2.Data}
|
||||
}
|
||||
})
|
||||
|
||||
// Use Local for second transformation (pure)
|
||||
local12 := Local[string](func(l1 Level1) Level2 {
|
||||
return Level2{Data: l1.Value}
|
||||
})
|
||||
|
||||
// Compose them
|
||||
effect2 := localReaderK23(effect3)
|
||||
effect1 := local12(effect2)
|
||||
|
||||
// Run
|
||||
ioResult := Provide[string](Level1{Value: "test"})(effect1)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "result", result)
|
||||
})
|
||||
|
||||
t.Run("runtime context deadline awareness", func(t *testing.T) {
|
||||
type Config struct {
|
||||
HasDeadline bool
|
||||
}
|
||||
|
||||
// Reader that checks runtime context for deadline
|
||||
checkContext := func(path string) reader.Reader[Config] {
|
||||
return func(ctx context.Context) Config {
|
||||
_, hasDeadline := ctx.Deadline()
|
||||
return Config{HasDeadline: hasDeadline}
|
||||
}
|
||||
}
|
||||
|
||||
// Effect that uses the config
|
||||
configEffect := Chain(func(cfg Config) Effect[Config, string] {
|
||||
return Of[Config](fmt.Sprintf("Has deadline: %v", cfg.HasDeadline))
|
||||
})(readerreaderioresult.Ask[Config]())
|
||||
|
||||
transform := LocalReaderK[string](checkContext)
|
||||
pathEffect := transform(configEffect)
|
||||
|
||||
// Without deadline
|
||||
ioResult := Provide[string]("config.json")(pathEffect)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Has deadline: false", result)
|
||||
|
||||
// With deadline
|
||||
ctxWithDeadline, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
ioResult2 := Provide[string]("config.json")(pathEffect)
|
||||
readerResult2 := RunSync(ioResult2)
|
||||
result2, err2 := readerResult2(ctxWithDeadline)
|
||||
|
||||
assert.NoError(t, err2)
|
||||
assert.Equal(t, "Has deadline: true", result2)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -61,6 +61,18 @@ func LocalReaderIOEitherK[A, C, E, R1, R2 any](f readerioeither.Kleisli[C, E, R2
|
||||
}
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func LocalReaderK[E, A, C, R1, R2 any](f reader.Kleisli[C, R2, R1]) func(ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
|
||||
return func(rri ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
|
||||
return F.Flow4(
|
||||
f,
|
||||
readerioeither.FromReader,
|
||||
readerioeither.Map[C, E](rri),
|
||||
readerioeither.Flatten,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func LocalReaderReaderIOEitherK[A, C, E, R1, R2 any](f Kleisli[R2, C, E, R2, R1]) func(ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
|
||||
return func(rri ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
|
||||
|
||||
Reference in New Issue
Block a user