better-py

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})  # 10

The 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})  # 15

Use 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)

See Also

  • IO - For side effect management
  • State - For state threading
  • Writer - For logging/accumulation

On this page