Logged — Accumulated Logs
Many systems require a record of how a computation arrived at its result: an audit trail of decisions made by a complex business rules engine, a diagnostic trace of steps inside a data transformation pipeline, or warning notices gathered during validation.
If we want to keep our code pure and testable, we typically resort to threading a log array manually:
This is incredibly noisy. Every single function in the chain must accept the log as a parameter and forward it, even when the function’s core mathematical logic has nothing to do with logging. If you add or remove a transformation step, you must manually adjust all variable threads.
The typical alternative is to inject a logging framework and call side effects (like console.log)
directly mid-function. While this removes parameter noise, it introduces global side effects. Our
functions are no longer pure; they cannot be tested in isolation without mocking the global output
stream, and we cannot programmatically inspect the logs to assert on business rules.
Logged<W, A> offers a clean, functional alternative. It is a simple data structure that pairs a
value A with an accumulated read-only array of log entries W:
Logging is decoupled from execution. The log is treated purely as immutable data. We build our pipeline step-by-step, letting the logs accumulate automatically, and decide what to do with them once at the boundary of our program.
Creating Logged Values
Section titled “Creating Logged Values”To begin logging, we lift our values into Logged using its core constructors:
Logged.tell represents the atomic logging block. It writes a single log entry and returns
undefined as its value, ready to be sequenced into a pipeline.
Transforming and Sequencing
Section titled “Transforming and Sequencing”We can transform the values inside Logged and sequence multiple logging steps point-free.
Transforming values with map
Section titled “Transforming values with map”map transforms the underlying value of a Logged container, leaving any accumulated log entries
completely untouched:
Sequencing logs with chain
Section titled “Sequencing logs with chain”chain is the key combinator for Logged. It passes the value of the current Logged container to
your next step, executes the step, and automatically concatenates the log arrays from both steps in
order:
The intermediate log arrays are stitched together by chain itself. Each individual step only
declares its own log entry, fully isolated from the history of the pipeline.
Practical Example: A Business Rules Engine
Section titled “Practical Example: A Business Rules Engine”Consider a discount calculator that applies a series of promotional codes. To audit decisions, each rule must record its reasoning:
The promotional rules remain completely independent. Neither applyMemberPromo nor applyBulkPromo
has any knowledge of the other’s existence or log records. The audit trail is built automatically
during sequencing.
Extracting Results with run
Section titled “Extracting Results with run”Logged.run unpacks the container and returns the value and accumulated log as a standard tuple:
By calling run at the boundary of your system, you can choose to write the logs to an external
database, return them to the client, or filter them, keeping your operational code 100% pure.
Accumulating values: bind / bindTo
Section titled “Accumulating values: bind / bindTo”When you need to perform multiple sequential operations and gather their results into a single
object, nesting chain and map inside pipelines can become highly complex:
To solve this, you can use bindTo and bind to cleanly accumulate values key-by-key in a flat,
readable pipeline.
bindTo lifts a value into the pipeline’s accumulator object:
bind runs a new operation using the accumulated object and attaches the result to a new key:
All logs produced at each key-binding step are automatically concatenated in sequential order.
When to use Logged
Section titled “When to use Logged”Use Logged when:
Section titled “Use Logged when:”- The log is an essential output: You are building rules engines, payload validators, or data migrators where the audit trail or warnings log must be returned to the caller or database.
- You require pure, testable traces: You want to assert on log traces programmatically in unit tests without setting up global console mocks or capturing standard output.
Keep using standard logging libraries when:
Section titled “Keep using standard logging libraries when:”- Logs are purely for development diagnostics: You are writing generic debugging logs that only humans will read in development, and the trace has no first-class programmatic value in production.