Reader Monad
Dependency injection and environment passing
The Reader monad represents computations that depend on a shared environment or context. It's commonly used for dependency injection and configuration management, allowing you to pass dependencies implicitly through a chain of operations.
Reader[E, A] is a function from an environment E to a value A. It allows you to pass dependencies implicitly and build computations that depend on a shared environment.
Creating & Running
Wrapping Environment Functions
The Reader constructor wraps a function that accesses the environment.
from better_py import Reader
# A Reader that accesses the environment
get_config = Reader(lambda config: config["database_url"])
# Run with an environment
config = {"database_url": "postgresql://localhost/mydb"}
get_config.run(config) # "postgresql://localhost/mydb"Getting the Environment
The Reader.ask method returns the entire environment.
from better_py import Reader
# Get the entire environment
env = Reader.ask()
env.run({"key": "value"}) # {"key": "value"}Use this when you need access to the full environment in your computation.
Running with Environment
The run method executes the Reader with a specific environment.
from better_py import Reader
reader = Reader(lambda env: env["key"])
reader.run({"key": "value"}) # "value"This is where you provide the actual environment for the computation to run.
Transforming Readers
Mapping Results
The map method transforms the result of a Reader computation.
from better_py import Reader
reader = Reader(lambda env: env["value"])
mapped = reader.map(lambda x: x * 2)
mapped.run({"value": 5}) # 10The transformation is applied to the result when the Reader is run.
Chaining Operations
The flat_map method chains Reader operations, passing the same environment through each step.
from better_py import Reader
def get_value(env):
return env["value"]
def double(x):
return Reader(lambda env: x * env["multiplier"])
reader = Reader(get_value).flat_map(double)
reader.run({"value": 5, "multiplier": 3}) # 15Use flat_map when subsequent operations depend on the previous result.
Modifying Environment Locally
The local method modifies the environment for a single computation without affecting the original.
from better_py import Reader
reader = Reader(lambda env: env["key"])
# Modify environment locally
modified = reader.local(lambda env: {**env, "key": "new value"})
modified.run({"key": "old value"}) # "new value"Use local to provide overridden or additional configuration for specific sub-computations.
Real-World Pattern: Configuration Validation
from better_py import Reader
def require_key(key: str):
return Reader(lambda env: env.get(key) or ValueError(f"Missing {key}"))
def validate_config():
return (require_key("database_url")
.flat_map(lambda db_url: require_key("redis_url")
.map(lambda redis_url: {"database_url": db_url, "redis_url": redis_url})))
valid_config = validate_config().run({
"database_url": "postgresql://localhost/db",
"redis_url": "redis://localhost/0"
}) # {"database_url": ..., "redis_url": ...}This pattern shows Reader's utility for validating configuration: all required keys are checked and collected into a dictionary, with any missing keys raising errors.
When to Use
Use Reader when:
- You need dependency injection
- You have shared configuration/environment
- You want implicit parameter passing
- You need to test with different environments
- You're building purely functional code
Don't use Reader when:
- The environment doesn't change (use plain parameters)
- You only have one dependency
- You're writing simple scripts (direct injection is fine)