Logged — values with accumulated logs
Many operations need to record what they did: an audit trail of decisions in a rules engine, a
debug trace of transformations in a data pipeline, a sequence of validation messages collected
alongside a processed value. The usual approach threads an array through every function as an extra
parameter. Logged<W, A> does that threading automatically: each step in a pipeline declares its
own log entries, and they are concatenated in order without any manual bookkeeping.
The problem with threading a log manually
Section titled “The problem with threading a log manually”The straightforward approach passes a log array into every function and returns a new one alongside the result:
Every function receives the log as a parameter and returns it as part of its result. Adding a new step means threading it through manually. Removing a step means adjusting every caller. And the log array has nothing to do with what the functions are actually computing — it’s just noise in every signature.
What a Logged is
Section titled “What a Logged is”A Logged<W, A> is a plain data structure — a value of type A paired with an ordered sequence
of log entries of type W. There are no side effects, no mutation, no console output. The log is
just data that you inspect or emit at the edge of your program.
Creating a Logged value
Section titled “Creating a Logged value”Logged.make wraps a value with an empty log:
Logged.tell records a single entry with no meaningful value. It is the atomic logging operation:
Transforming with map
Section titled “Transforming with map”map changes the value without touching the log:
Any log entries already present are carried forward unchanged:
Sequencing with chain
Section titled “Sequencing with chain”chain is the key operation. It passes the value of one Logged to a function that returns
another Logged, and automatically concatenates both logs:
No function in the chain touches the log from a previous step — the concatenation is handled by
chain itself. Each step only declares its own entries.
A rules engine example
Section titled “A rules engine example”Suppose you are applying a sequence of business rules to a discount calculation. Each rule may apply a modifier and should record its reasoning:
The audit trail builds automatically. Neither memberDiscount nor bulkDiscount knows about the
other’s log entries — chain stitches them together.
A transformation pipeline with debug trace
Section titled “A transformation pipeline with debug trace”In a data transformation pipeline you often want to know what each step produced without wiring in actual logging infrastructure:
Extracting results with run
Section titled “Extracting results with run”Logged.run returns the value and log as a tuple [value, log]. Call it at the boundary where
you want to act on the results — emit the log to a monitoring system, return both to the caller, or
discard one:
When to use Logged
Section titled “When to use Logged”- You want a computation to produce both a result and a record of what happened, without threading a log array through every function signature.
- You are building a rules engine, validation pipeline, or data transformation where each step should declare its own reasoning and the final caller collects the full trace.
- You want pure, testable logging — the log is just data, there are no side effects until you explicitly emit it.
Keep using plain logging calls when the output is purely for human debugging during development
and you don’t need to inspect, assert on, or forward the log programmatically. Logged is most
valuable when the log itself is a first-class output that callers need to process, not just a
side channel.