Reader — deferred dependencies
Some values belong to the pipeline, not to any individual function — a database connection, an API
config, a locale. Yet they end up threaded through every signature as an extra parameter, repeated
at every call site, cluttering code that doesn’t actually use them. Reader<R, A> lets you describe
a computation that needs an environment R to produce A, compose it freely, and supply R once
at the edge of your program.
The problem with parameter drilling
Section titled “The problem with parameter drilling”When multiple functions in a pipeline all need the same input, that input ends up in every signature even when most functions only forward it:
endpoint doesn’t use config for anything other than passing it along. As pipelines deepen, this
pattern becomes noise — every signature carries a parameter that belongs to the pipeline, not the
function.
The Reader approach
Section titled “The Reader approach”With Reader, each function returns a description of the computation it intends to perform, rather than accepting the dependency as an argument. The pipeline is built first, and the dependency flows through automatically when it is supplied once at the end:
No function accepts apiConfig directly. Each step declares what it reads from the configuration,
the composition wires it together, and Reader.run injects the value once.
Creating Readers
Section titled “Creating Readers”Reader.asks is the primary constructor. It builds a Reader that projects a value from R:
Reader.ask returns the entire R unchanged, when you need to pass it whole to something else:
Reader.resolve lifts a pure value that needs nothing from R:
Transforming with map
Section titled “Transforming with map”map transforms the value a Reader produces. The environment passes through unchanged.
Consider locale-aware formatting, where rendering an amount correctly depends on the display context — but that context is not a property of the amount itself:
The same Reader, two different environments. R is the rendering context; the amounts are ordinary
arguments.
Sequencing with chain
Section titled “Sequencing with chain”chain sequences two Readers where the second depends on the output of the first. Both receive the
same R:
Each step reads from the locale independently. chain threads the environment through without any
step having to accept or forward it explicitly.
Adapting environments with local
Section titled “Adapting environments with local”Different parts of a program often need different slices of the total environment. local adapts a
Reader that expects a narrow type to work inside a broader one, by providing the extraction function:
connectionString and authHeader are independently useful Readers with narrow, precise
requirements. local lifts them into AppEnv without changing their implementations. Library
functions declare only what they need; application code composes them using local.
Applying wrapped functions with ap
Section titled “Applying wrapped functions with ap”ap applies a function wrapped in a Reader to a value wrapped in a Reader. Both Readers see the
same environment. This is useful when you need to combine the outputs of multiple dependent
computations:
ap sequences applications where the function and value both depend on the environment.
Side effects with tap
Section titled “Side effects with tap”tap runs a side effect on the produced value and returns it unchanged — useful for logging in the
middle of a pipeline:
Running with run
Section titled “Running with run”Reader.run executes a Reader by supplying the environment. It is the data-last equivalent of
calling the function directly:
Reader.run fits naturally at the end of a pipe chain. Call it once, at the point in your
program where the environment is available.
When to use Reader
Section titled “When to use Reader”Use Reader when:
- Multiple steps in a pipeline all need the same input and you want to avoid threading it through every function signature
- You want to compose functions with narrow, precise requirements into a broader context using
local - You want to run the same pipeline against different inputs — different locales, test vs. production config, different strategies — without changing the pipeline itself
Keep passing arguments directly when:
- Only one or two functions need the value — the overhead of Reader is not worth it
- The value changes between calls in a way that belongs in the function signature, not the environment